Advanced Java

Build Tools

Module 1

Maven for Modern Java Development & TDD

What is a Build Tool?

A build tool automates the development workflow:

Why Do We Need Build Tools?

Without Build Tools:

  • Manual dependency downloads
  • Inconsistent project structure
  • Complex compilation commands
  • No standardized process
  • Difficult collaboration

With Build Tools:

  • Automated dependencies
  • Standardized layout
  • One-command builds
  • Reproducible builds
  • CI/CD integration

Why Maven?

Maven has been the de-facto standard for Java projects since 2004.


Key Advantages:

Maven Project Structure

my-project/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/          # Application source code
│   │   ├── resources/     # Configuration files
│   │   └── webapp/        # Web resources (for web apps)
│   └── test/
│       ├── java/          # Test source code
│       └── resources/     # Test configuration
└── target/                # Compiled output (generated)

The POM File: Project Object Model

The heart of every Maven project is pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.example</groupId>
    <artifactId>my-application</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
</project>

Maven Coordinates: GAV

Every Maven artifact is uniquely identified by GroupId, ArtifactId, Version:

<groupId>com.example</groupId>
<artifactId>my-application</artifactId>
<version>1.0.0-SNAPSHOT</version>

Real-world examples:

Adding Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.1</version>
        <scope>runtime</scope>
    </dependency>
    
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Dependency Scopes

Scope Description Example
compile Default, available everywhere Spring Framework
provided Provided by runtime environment Servlet API
runtime Not needed for compilation JDBC drivers
test Only for testing JUnit, Mockito
import Import dependencies from BOM Spring Boot BOM

Transitive Dependencies

Maven automatically resolves dependencies of your dependencies!

Your Project
├── spring-boot-starter-web:3.2.0
    ├── spring-boot-starter:3.2.0
    ├── spring-boot-starter-json:3.2.0
    ├── spring-boot-starter-tomcat:3.2.0
    ├── spring-web:6.1.0
    └── spring-webmvc:6.1.0
        ├── spring-beans:6.1.0
        ├── spring-context:6.1.0
        └── spring-core:6.1.0

Result: You declare one dependency, Maven downloads 20+ libraries!

Maven Build Lifecycle

Maven has three built-in lifecycles. The Default Lifecycle (most common):

  1. validate - Validate project correctness
  2. compile - Compile source code
  3. test - Run unit tests
  4. package - Package compiled code (JAR/WAR)
  5. verify - Run integration tests
  6. install - Install package to local repository
  7. deploy - Deploy to remote repository

Common Maven Commands

# Clean the target directory
mvn clean

# Compile the code
mvn compile

# Run tests
mvn test

# Create JAR/WAR
mvn package

# Install to local repo (~/.m2/repository)
mvn install

# Clean and build from scratch
mvn clean install

# Skip tests
mvn clean install -DskipTests

Properties in Maven

Define reusable values to avoid duplication:

<properties>
    <java.version>17</java.version>
    <spring.version>6.1.0</spring.version>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

Dependency Management

Centralize dependency versions using dependencyManagement:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- No version needed - inherited from parent -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Maven Plugins

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <source>17</source>
                <target>17</target>
            </configuration>
        </plugin>
        
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>3.2.0</version>
        </plugin>
    </plugins>
</build>

Multi-Module Projects

Organize large projects into modules:

parent-project/
├── pom.xml (packaging: pom)
├── core-module/
│   └── pom.xml
├── web-module/
│   └── pom.xml
└── data-module/
    └── pom.xml

<packaging>pom</packaging>
<modules>
    <module>core-module</module>
    <module>web-module</module>
    <module>data-module</module>
</modules>

Maven Wrapper

The Maven Wrapper ensures consistent Maven version across teams:

# Generate wrapper files
mvn wrapper:wrapper -Dmaven=3.9.6

# Use wrapper instead of installed maven
./mvnw clean install    # Unix/Mac/Linux
mvnw.cmd clean install  # Windows

Benefits:

Exercise 1: Create Your First Maven Project

Task: Create a simple calculator application with Maven


Requirements:

  1. Create a Maven project with proper structure
  2. Implement Calculator class with add, subtract, multiply, divide
  3. Add JUnit 5 dependency
  4. Write unit tests for all methods
  5. Build and run tests with Maven

Exercise 2: Multi-Module Maven Project

Task: Create a multi-module project for a web application

myapp-parent/
├── pom.xml (parent)
├── myapp-core/         # Business logic
├── myapp-data/         # Data access layer
└── myapp-web/          # Web layer (depends on core and data)

Requirements:

Exercise 3: Dependency Analysis

Task: Analyze and optimize dependencies

  1. Create a Spring Boot project
  2. Add multiple dependencies
  3. Run dependency tree analysis: mvn dependency:tree
  4. Identify transitive dependencies
  5. Find and resolve dependency conflicts
  6. Use dependency exclusions if needed

Best Practices

1. Version Management

2. Repository Management

3. Build Configuration

Introduction to Testing

Testing is crucial for maintaining code quality and catching bugs early.


Types of Tests:


Benefits:

Test-Driven Development (TDD)

TDD is a development approach where tests are written before code.


The TDD Cycle (Red-Green-Refactor):

  1. Red: Write a failing test
  2. Green: Write minimal code to make it pass
  3. Refactor: Improve the code while keeping tests green

TDD Benefits:

JUnit 5 (Jupiter)

JUnit 5 is the modern standard for Java unit testing.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    @Test
    void shouldAddTwoNumbers() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result);
    }
}

JUnit 5 Assertions

import static org.junit.jupiter.api.Assertions.*;

@Test
void testAssertions() {
    // Basic assertions
    assertEquals(expected, actual);
    assertNotEquals(unexpected, actual);
    assertTrue(condition);
    assertFalse(condition);
    assertNull(object);
    assertNotNull(object);
    
    // Array/Collection assertions
    assertArrayEquals(expectedArray, actualArray);
    assertIterableEquals(expected, actual);
    
    // Exception assertions
    assertThrows(IllegalArgumentException.class, () -> {
        // Code that should throw exception
    });
    
    // Timeout assertions
    assertTimeout(Duration.ofSeconds(1), () -> {
        // Fast operation
    });
}

JUnit 5 Lifecycle

class UserServiceTest {
    
    private UserService userService;
    
    @BeforeAll
    static void initAll() {
        // Run once before all tests
        System.out.println("Starting test suite");
    }
    
    @BeforeEach
    void init() {
        // Run before each test
        userService = new UserService();
    }
    
    @Test
    void testCreateUser() {
        // Test code
    }
    
    @AfterEach
    void tearDown() {
        // Run after each test
        userService = null;
    }
    
    @AfterAll
    static void tearDownAll() {
        // Run once after all tests
        System.out.println("Test suite completed");
    }
}

Parameterized Tests

Run the same test with different inputs:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class StringUtilsTest {
    
    @ParameterizedTest
    @ValueSource(strings = {"", "  ", "\t", "\n"})
    void shouldDetectBlankStrings(String input) {
        assertTrue(StringUtils.isBlank(input));
    }
    
    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "5, 5, 10"
    })
    void shouldAddNumbers(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }
    
    @ParameterizedTest
    @MethodSource("provideTestData")
    void testWithMethodSource(String input, int expected) {
        assertEquals(expected, process(input));
    }
    
    static Stream provideTestData() {
        return Stream.of(
            Arguments.of("test", 4),
            Arguments.of("hello", 5)
        );
    }
}

TDD Example: Calculator

Step 1: Write the Test (Red)

@Test
void shouldDivideTwoNumbers() {
    Calculator calc = new Calculator();
    double result = calc.divide(10, 2);
    assertEquals(5.0, result, 0.001);
}

@Test
void shouldThrowExceptionWhenDividingByZero() {
    Calculator calc = new Calculator();
    assertThrows(ArithmeticException.class, 
        () -> calc.divide(10, 0));
}

Tests fail - divide() method doesn't exist yet

TDD Example: Calculator

Step 2: Write Minimal Code (Green)

public class Calculator {
    
    public double divide(double a, double b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

Tests pass!


Step 3: Refactor

public class Calculator {
    
    public double divide(double dividend, double divisor) {
        validateDivisor(divisor);
        return dividend / divisor;
    }
    
    private void validateDivisor(double divisor) {
        if (divisor == 0) {
            throw new ArithmeticException("Division by zero");
        }
    }
}

Running Tests with Maven

Maven Surefire plugin runs tests automatically:

# Run all tests
mvn test

# Run specific test class
mvn test -Dtest=CalculatorTest

# Run specific test method
mvn test -Dtest=CalculatorTest#shouldAddTwoNumbers

# Skip tests
mvn install -DskipTests

# Run tests in parallel
mvn test -Dparallel=classes -DthreadCount=4

Maven Surefire Configuration:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.3</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
        </includes>
    </configuration>
</plugin>

Code Coverage with JaCoCo

Measure how much of your code is covered by tests:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>
mvn test jacoco:report
# Report generated in target/site/jacoco/index.html

Exercise: TDD Practice

Task: Build a StringCalculator using TDD


Requirements (implement one at a time):

  1. Create a method add(String numbers) that returns sum
  2. Empty string returns 0
  3. Single number returns that number: "5" → 5
  4. Two numbers comma-separated: "1,2" → 3
  5. Handle multiple numbers: "1,2,3,4" → 10
  6. Handle newlines as delimiters: "1\n2,3" → 6
  7. Throw exception for negative numbers

Remember: Write test first, then make it pass!

Modern Build Tool Trends (2025)

1. Native Image Compilation

GraalVM native-image for faster startup and lower memory footprint

2. Build Reproducibility

Deterministic builds and verifiable artifacts

3. Supply Chain Security

4. Cloud-Native Builds

Logging: Why It Matters

Logging is essential for debugging, monitoring, and understanding application behavior.


Why Use a Logging Framework?

Log4j 2: Modern Logging

Log4j 2 is the modern, high-performance logging framework for Java.


Why Log4j 2?

Adding Log4j 2 to Maven

<dependencies>
    <!-- Log4j 2 API -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.22.1</version>
    </dependency>

    <!-- Log4j 2 Implementation -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.22.1</version>
    </dependency>
</dependencies>

Note: Always use the same version for both API and Core

Using Log4j 2 in Code

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class UserService {
    private static final Logger logger = LogManager.getLogger(UserService.class);

    public void createUser(User user) {
        logger.info("Creating user: {}", user.getEmail());

        try {
            // Business logic
            userRepository.save(user);
            logger.debug("User saved successfully with ID: {}", user.getId());
        } catch (Exception e) {
            logger.error("Failed to create user: {}", user.getEmail(), e);
            throw e;
        }
    }

    public void deleteUser(Long id) {
        logger.warn("Deleting user with ID: {}", id);
        userRepository.deleteById(id);
    }
}

Log Levels

Level Purpose When to Use
TRACE Very detailed diagnostic Debugging complex flows
DEBUG Debugging information Development and troubleshooting
INFO Informational messages Key application events
WARN Potentially harmful situations Recoverable issues
ERROR Error events Application errors
FATAL Severe errors Application-stopping issues

log4j2.xml Configuration

Create src/main/resources/log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <!-- Console Appender -->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>

        <!-- File Appender -->
        <File name="File" fileName="logs/application.log">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n"/>
        </File>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

Advanced log4j2.xml Configuration

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout>
                <Pattern>%d{HH:mm:ss.SSS} %highlight{%-5level} %style{%logger{36}}{cyan} - %msg%n</Pattern>
            </PatternLayout>
        </Console>

        <!-- Rolling File Appender -->
        <RollingFile name="RollingFile" fileName="logs/app.log"
                     filePattern="logs/app-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="10 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingFile>
    </Appenders>

    <Loggers>
        <Logger name="com.example" level="debug" additivity="false">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="RollingFile"/>
        </Logger>

        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

Pattern Layout Explained

Understanding the pattern format:

%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg%n

Pattern Description Example
%d{...} Date/time 2025-01-15 14:30:45.123
%-5level Log level (left-aligned, 5 chars) INFO, ERROR
%t Thread name main, pool-1-thread-1
%logger{36} Logger name (max 36 chars) com.example.UserService
%msg Log message User created successfully
%n Newline \n

Logging Best Practices

1. Use Parameterized Messages

// Good - lazy evaluation
logger.info("User {} logged in from {}", username, ipAddress);

// Bad - string concatenation always executes
logger.info("User " + username + " logged in from " + ipAddress);

2. Appropriate Log Levels

3. Don't Log Sensitive Data

// Bad
logger.info("Password: {}", user.getPassword());

// Good
logger.info("User {} authenticated successfully", user.getUsername());

Summary

In this module, you learned:


Next Module: Dependency Injection & Spring Framework

Resources

Maven:


Testing & Logging:

Slide Overview