Module 28 - Build Tools

Maven and Gradle both compile code, resolve dependencies, run tests, and package artifacts. This module covers Maven in depth and introduces Gradle as its modern counterpart.


Maven

Three Lifecycles

Maven has three independent lifecycles. Every lifecycle is a sequence of phases; running a phase runs all phases before it.

┌──────────────────────────────────────────────────────────────────────────────┐
│  DEFAULT lifecycle  (most used)                                              │
│                                                                              │
│  validate → compile → test → package → verify → install → deploy            │
│      │          │        │       │          │        │          │            │
│      │          │        │       │          │        │          └ push to    │
│      │          │        │       │          │        │            remote     │
│      │          │        │       │          │        └ copy JAR to ~/.m2     │
│      │          │        │       │          └ integration-tests              │
│      │          │        │       └ create JAR / WAR / EAR                   │
│      │          │        └ compile test sources; run unit tests              │
│      │          └ compile src/main/java  →  target/classes                  │
│      └ check pom.xml is well-formed; project structure is valid             │
│                                                                              │
│  Common invocations:                                                         │
│    mvn compile       runs: validate → compile                                │
│    mvn test          runs: validate → compile → test                         │
│    mvn package       runs: validate → compile → test → package               │
│    mvn install       runs: ... → package → install                           │
│    mvn deploy        runs: all phases                                        │
├──────────────────────────────────────────────────────────────────────────────┤
│  CLEAN lifecycle                                                             │
│                                                                              │
│  pre-clean → clean → post-clean                                             │
│                  └ deletes target/                                          │
├──────────────────────────────────────────────────────────────────────────────┤
│  SITE lifecycle                                                              │
│                                                                              │
│  pre-site → site → post-site → site-deploy                                 │
│                └ generates HTML docs in target/site/                        │
└──────────────────────────────────────────────────────────────────────────────┘

Phases vs Goals

  Phase      = a step in a lifecycle  (e.g. compile, test, package)
  Goal       = a plugin task          (e.g. compiler:compile, surefire:test)
  Binding    = a goal attached to a phase

  mvn test                  → runs the test phase (and everything before it)
  mvn surefire:test         → runs only the surefire test goal directly
  mvn compiler:compile test → compile goal + test phase

Maven Multi-Module Projects

A multi-module (reactor) build groups related modules under one parent POM. mvn install at the root builds all modules in dependency order automatically.

  module-28-build-tools/             ← parent POM  (packaging = pom)
  │   pom.xml
  │     └ <packaging>pom</packaging>
  │     └ <modules>
  │         <module>calculator-api</module>
  │         <module>calculator-impl</module>
  │       </modules>
  │     └ <dependencyManagement> - version pinning for all children
  │     └ <build><pluginManagement> - plugin config for all children
  │     └ <profiles> - optional build variants
  │
  ├── calculator-api/                 ← API module  (no implementation)
  │   pom.xml
  │     └ <parent> → module-28-build-tools
  │
  └── calculator-impl/                ← Implementation module
      pom.xml
        └ <parent> → module-28-build-tools
        └ <dependency> on calculator-api  ← inter-module dependency

Build order - Maven resolves the dependency graph and builds calculator-api before calculator-impl automatically, even if you list them in reverse order in <modules>.

dependencyManagement vs dependencies

<!-- Parent POM - pins the version, does NOT add the dependency -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>   <!-- version set here once -->
            <scope>test</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- Child POM - opts in, but omits <version> -->
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <!-- <version> inherited from parent's dependencyManagement -->
        <scope>test</scope>
    </dependency>
</dependencies>

Maven Dependency Scopes

  Scope          Compile  Test  Runtime  Transitive  Typical Use
  ─────────────  ───────  ────  ───────  ──────────  ──────────────────────────
  compile (def)    ✓        ✓     ✓        ✓         jackson-databind, guava
  test             ✗        ✓     ✗        ✗         junit-jupiter, mockito
  provided         ✓        ✓     ✗        ✗         servlet-api (server provides it)
  runtime          ✗        ✓     ✓        ✓         postgresql JDBC driver
  system           ✓        ✓     ✗        ✗         avoid - uses absolute local path

  Compile = on the compiler's classpath
  Runtime = on the JVM classpath when the app runs
  Transitive = propagated to modules that depend on this one

Maven Profiles

Profiles activate alternate build configurations:

<profiles>
    <!-- mvn package -P fast  →  skip tests -->
    <profile>
        <id>fast</id>
        <properties>
            <skipTests>true</skipTests>
        </properties>
    </profile>

    <!-- mvn package -P strict  →  all warnings as errors -->
    <profile>
        <id>strict</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <compilerArgs>
                            <arg>-Xlint:all</arg>
                            <arg>-Werror</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Other activation strategies: OS, environment variable, JDK version, file presence.


Useful Maven Commands

mvn compile                   # compile only
mvn test                      # compile + run tests
mvn package                   # build JAR/WAR
mvn package -DskipTests       # skip tests (faster packaging)
mvn package -P fast           # activate 'fast' profile
mvn install                   # install to local ~/.m2 repository
mvn dependency:tree           # show full dependency tree
mvn dependency:analyze        # find unused / undeclared deps
mvn versions:display-dependency-updates   # list available upgrades
mvn help:effective-pom        # show fully resolved POM
mvn -pl calculator-impl test  # run tests in one module only
mvn -pl calculator-impl -am test  # also build upstream modules (-am)

Gradle

Three Build Phases

┌───────────────────────────────────────────────────────────────────────────┐
│  1. INITIALIZATION                                                        │
│     Read settings.gradle.kts                                              │
│     Determine which projects are part of the build                        │
│     Create a Project object for each                                      │
├───────────────────────────────────────────────────────────────────────────┤
│  2. CONFIGURATION                                                         │
│     Evaluate every build.gradle.kts (root + all subprojects)             │
│     Configure all tasks and their dependencies                            │
│     Build the task execution graph                                        │
│     (All tasks are configured even if not executed)                       │
├───────────────────────────────────────────────────────────────────────────┤
│  3. EXECUTION                                                             │
│     Run only the requested tasks in dependency order                      │
│     Skip up-to-date tasks (incremental build)                            │
│     Use the build cache for tasks with matching inputs (if enabled)       │
└───────────────────────────────────────────────────────────────────────────┘

Gradle Dependency Configurations

  Configuration      Compile  Runtime  Exposed to Consumers  Maven Equivalent
  ─────────────────  ───────  ───────  ────────────────────  ─────────────────
  implementation       ✓        ✓       ✗ (encapsulated)      compile (no leak)
  api                  ✓        ✓       ✓ (java-library plugin) compile (transitive)
  compileOnly          ✓        ✗       ✗                     provided
  runtimeOnly          ✗        ✓       ✗                     runtime
  testImplementation   ✓ (test) ✓ (test) ✗                   test
  testRuntimeOnly      ✗        ✓ (test) ✗                   test + runtime

implementation vs api

  Use implementation (default):
    compile-time dependency is hidden from consumers
    faster incremental recompilation (consumer only recompiles if API changes)

  Use api (requires java-library plugin):
    dependency leaks through to consumers
    consumers can use classes from your dependency directly

Gradle Multi-Project Build

  gradle-demo/
  ├── settings.gradle.kts          ← declares the project structure
  │     rootProject.name = "gradle-demo"
  │     include("greeter-api", "greeter-impl")
  │
  ├── build.gradle.kts             ← root: shared config via subprojects { }
  │     subprojects {
  │         apply(plugin = "java")
  │         repositories { mavenCentral() }
  │         dependencies { testImplementation(...) }
  │         tasks.withType<Test> { useJUnitPlatform() }
  │     }
  │
  ├── greeter-api/
  │   └── build.gradle.kts         ← no extra config (inherits from root)
  │
  └── greeter-impl/
      └── build.gradle.kts
            dependencies {
                implementation(project(":greeter-api"))   ← project dependency
            }

Maven vs Gradle - Side-by-Side

  Feature                  Maven                        Gradle (Kotlin DSL)
  ───────────────────────  ───────────────────────────  ─────────────────────────────
  Build file               pom.xml                      build.gradle.kts
  Language                 XML (declarative)            Kotlin/Groovy (imperative)
  Multi-module             <modules> in parent POM      include() in settings.kts
  Version pinning          <dependencyManagement>       platform() BOM or version catalog
  Plugin                   <plugin> in <build>          plugins { id("...") }
  Custom build logic       Mojo (Java class)            task { doLast { ... } }
  Incremental build        Limited                      First-class (input/output tracking)
  Build cache              No                           Yes (local + remote)
  Wrapper                  mvnw / .mvn/wrapper/         gradlew / gradle/wrapper/
  Android support          No                           Yes (required)
  Default test engine      Surefire detects JUnit       useJUnitPlatform() needed
  Parallel execution       -T option                    Default for independent tasks

Gradle Build File Anatomy

// build.gradle.kts

plugins {
    java                                    // apply the Java plugin
    // id("application")                   // apply a community/ecosystem plugin
}

group   = "com.javatraining"
version = "1.0.0"

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))   // reproducible JDK
    }
}

repositories {
    mavenCentral()                         // resolve from Maven Central
}

dependencies {
    implementation("com.google.guava:guava:33.2.0-jre")          // compile + runtime
    compileOnly("org.projectlombok:lombok:1.18.32")              // compile only
    runtimeOnly("org.postgresql:postgresql:42.7.3")              // runtime only
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") // test only
}

tasks.test {
    useJUnitPlatform()     // enable JUnit 5
    maxParallelForks = Runtime.getRuntime().availableProcessors()
}

// Custom task
tasks.register("hello") {
    doLast { println("Hello from ${project.name}!") }
}

Gradle Wrapper

The Gradle wrapper ensures every developer and CI server uses the exact same Gradle version - no installation required.

  gradle-demo/
  ├── gradlew               ← Unix shell script  (commit this)
  ├── gradlew.bat           ← Windows batch file (commit this)
  └── gradle/wrapper/
      ├── gradle-wrapper.jar         ← bootstrap binary (commit this)
      └── gradle-wrapper.properties  ← points to specific Gradle version
# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
./gradlew tasks              # list available tasks
./gradlew test               # compile + run tests
./gradlew build              # compile + test + JAR
./gradlew :greeter-impl:test # run tests in one subproject
./gradlew dependencies       # show dependency tree
./gradlew --build-cache test # reuse outputs from cache

Incremental Builds

Gradle tracks inputs and outputs for every task. If they haven’t changed since the last run, the task is skipped:

  $ ./gradlew test
  > Task :greeter-api:compileJava UP-TO-DATE   ← nothing changed, skipped
  > Task :greeter-api:classes     UP-TO-DATE
  > Task :greeter-impl:compileJava UP-TO-DATE
  > Task :greeter-impl:test       UP-TO-DATE   ← tests skipped
  BUILD SUCCESSFUL in 0s

  Maven always reruns the full lifecycle phase - no equivalent optimisation.

Module 28 - What Was Built

This module is a Maven multi-module project:

  module-28-build-tools/   ← parent POM (packaging=pom)
  ├── pom.xml                dependencyManagement, pluginManagement, profiles
  ├── calculator-api/        Calculator interface + MathUtils  (28 tests)
  │   ├── pom.xml            parent ref; junit test dep (no version)
  │   └── src/
  └── calculator-impl/       BasicCalculator + ScientificCalculator (27 tests)
      ├── pom.xml            parent ref; dep on calculator-api; junit test dep
      └── src/

Gradle equivalent lives in gradle-demo/ - same two-module structure using Kotlin DSL (settings.gradle.kts, build.gradle.kts, subproject build files). Run it with ./gradlew build if Gradle is installed, or generate the wrapper:

gradle wrapper --gradle-version 8.8
./gradlew build