Grokking Spring Boot
Vote

0% completed

​

Build Tools: Maven and Gradle

Coordinates and Repositories

Dependencies and the Tree They Form

Scopes: Which Classpath Gets a Library

The Maven Lifecycle

Gradle: Tasks Instead of Phases

Maven and Gradle Side by Side

Key Takeaways

Practice Questions

Between its source files and a running jar, the booking service needs four jobs done:

  • Download the libraries it declares, and the libraries those libraries need.
  • Compile the main code and the tests.
  • Run the tests.
  • Package everything into one executable jar.

A build tool does these jobs, and the two common ones for Java are Maven and Gradle. Each asks for a library in its own way. This is the request in pom.xml, the build file of Maven:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency>

This is the same request in build.gradle, the build file of Gradle:

implementation 'org.springframework.boot:spring-boot-starter-webmvc'

Maven describes the build as XML data, while Gradle describes it as a short program. We built the booking service with both, and each produced an executable jar. The ideas underneath are shared: coordinates, dependencies, scopes, an ordered build, and a wrapper. The sections below explain those ideas, then compare the two tools.

Coordinates and Repositories

Every library is identified by three coordinates: a groupId for the organization, an artifactId for the library, and a version. They are often written joined by colons. The H2 database in the booking service is com.h2database:h2:2.4.240. The Gradle line above uses the same form, with the version left out. The Spring Boot plugin and the dependency-management plugin supply it from Spring Boot's list of managed versions.

A build tool downloads libraries from a repository, a server that stores published libraries by their coordinates. The usual one is Maven Central, and both tools use it. Downloaded files are kept in a local cache. On our machine, Maven stored them under ~/.m2/repository, and Gradle created its own folder, ~/.gradle, on its first run.

Dependencies and the Tree They Form

A library you declare usually depends on other libraries, called transitive dependencies, and the build tool resolves the whole tree. Our copy of the booking service has data access as well as web support. Maven printed its declared dependencies like this (the first level only):

+- org.springframework.boot:spring-boot-starter-webmvc:jar:4.1.2:compile +- org.springframework.boot:spring-boot-starter-data-jpa:jar:4.1.2:compile +- com.h2database:h2:jar:2.4.240:runtime +- org.springframework.boot:spring-boot-starter-data-jpa-test:jar:4.1.2:test \- org.springframework.boot:spring-boot-starter-webmvc-test:jar:4.1.2:test

The three declarations that are not test-only became 65 library jars inside the packaged application. The Spring Boot plugin added one more jar of its own, spring-boot-jarmode-tools, for 66 in total. Sometimes two parts of the tree ask for different versions of the same library. Maven then uses the version closest to your project in the tree, which its documentation calls the nearest definition.

The last word on each line is the scope, the subject of the next section.

Scopes: Which Classpath Gets a Library

A scope decides on which classpaths a dependency appears: compiling the main code, running it, or compiling and running tests. The classpath, as defined in What Spring Boot Is, is the list of libraries a program can load. Maven calls these sets scopes, and Gradle calls them configurations. They map closely:

Maven scopeGradle configurationCompile mainRun mainTests
compileimplementationYesYesYes
runtimeruntimeOnlyNoYesMaven yes, Gradle only when run
providedcompileOnlyYesNo, supplied by the platformMaven yes, Gradle no
testtestImplementationNoNoYes

The documentation of each tool defines these sets. A dependency with Maven's runtime scope, for example, is not required for compilation but is required for execution, and Maven also puts it on the test classpath. Maven's provided scope expects the JDK or a container to provide the dependency at runtime, and Maven still adds it to the classpath for compiling and running tests. Gradle's compileOnly does not reach the tests, which use testImplementation and testCompileOnly instead.

The booking service shows why scopes matter. H2 has runtime scope, because no class of the service refers to H2 directly. Spring loads its driver when the application runs. The test starters have test scope. In the packaged jar we found h2-2.4.240.jar in BOOT-INF/lib, but no JUnit, AssertJ, or Mockito jar.

Three dependencies of the booking service and the classpaths each one reaches, ending with what the packaged jar contains.
Three dependencies of the booking service and the classpaths each one reaches, ending with what the packaged jar contains.

Notice the row for H2. It is missing from the compile column, so main code that imports an H2 class would not compile, yet it is present when the application runs.

The Maven Lifecycle

Maven's build is a lifecycle, an ordered list of phases, and asking for one phase runs every phase before it. The documentation lists the main phases in order: validate, compile, test, package, verify, install, and deploy. Plugin goals, small units of work that plugins provide, carry out each phase.

When we ran ./mvnw -B package on the booking service, Maven printed each goal as it ran:

[INFO] --- resources:3.5.0:resources (default-resources) @ booking --- [INFO] --- compiler:3.15.0:compile (default-compile) @ booking --- [INFO] --- resources:3.5.0:testResources (default-testResources) @ booking --- [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ booking --- [INFO] --- surefire:3.5.6:test (default-test) @ booking --- [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] --- jar:3.5.1:jar (default-jar) @ booking --- [INFO] --- spring-boot:4.1.2:repackage (repackage) @ booking --- [INFO] BUILD SUCCESS

The order follows the phases. Resources are copied and code is compiled, then tests are compiled and run, and only then is the jar built. The last goal, repackage, comes from the Spring Boot plugin. It turns the plain jar into the executable jar from Running and Packaging an Application. Running ./mvnw -B compile instead stopped after the first two goals.

The package phase runs every earlier phase in order, each through a plugin goal, and a failing test stops the build before the jar.
The package phase runs every earlier phase in order, each through a plugin goal, and a failing test stops the build before the jar.

Notice that the tests run between compiling and packaging. A failing test stops the build, so no jar is produced from code whose tests fail.

Gradle: Tasks Instead of Phases

Gradle's build is a set of tasks, each of which declares the tasks it depends on. Gradle runs only the tasks needed for the one you ask for. Here is the build file the Initializr generated, without its group, version, and Java lines:

plugins { id 'java' id 'org.springframework.boot' version '4.1.2' id 'io.spring.dependency-management' version '1.1.7' } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }

The Spring Boot plugin adds a task named bootJar, which its documentation says builds executable jars. Our first ./gradlew build ran 13 tasks, among them compileJava, processResources, bootJar, compileTestJava, and test. It took 1 minute 30 seconds, which included the wrapper downloading Gradle 9.8.2. The jar appeared in build/libs instead of target. Gradle's summary counted 7 of the 13 as actionable tasks, the ones that do real work, and reported "7 actionable tasks: 7 executed".

A second run with no changes reported "7 actionable tasks: 7 up-to-date" and finished in 6 seconds, one run each on one laptop. The Gradle documentation explains the difference. Gradle tests whether a task's inputs or outputs changed since the last build, and if not, it skips the task. Gradle also keeps a background process, the daemon, between builds. When we repeated the unchanged build after the daemon had stopped, it took 1 minute 8 seconds, because the daemon had to start again.

Maven and Gradle Side by Side

TopicMavenGradle
Build filepom.xml, XML databuild.gradle, a Groovy or Kotlin program
Unit of workPhases, carried out by plugin goalsTasks with dependencies between them
Declaring a dependencyA dependency element with a scopeOne line in a configuration such as implementation
Output foldertargetbuild/libs
Wrapper./mvnw./gradlew
Repeated build, no changesRuns the goals againSkips up-to-date tasks

Both tools work well with Spring Boot, and the Initializr generates either. Maven's fixed lifecycle makes every project look alike. Gradle's build files allow more custom logic, which also needs more discipline. This course uses Maven, because its output is easier to read in a lesson.

Key Takeaways

  • A library is named by its coordinates, groupId, artifactId, and version, and is downloaded from a repository into a local cache.
  • Declared dependencies bring transitive ones, and three declarations of the booking service became 65 of the 66 jars packaged into the application.
  • Scopes and configurations decide which classpaths get a library, and test libraries never reach the packaged jar.
  • Maven runs ordered phases through plugin goals, and the package phase runs the tests before building the jar.
  • Gradle runs tasks with dependencies and skips the ones whose inputs have not changed.

That completes the Java and build background the service needs. Wiring Objects by Hand returns to the main method of the booking service and the cost of connecting objects without a container.

Practice Questions

1. A developer adds the PostgreSQL driver with compile scope, and a colleague suggests runtime scope instead. What does the change protect against?

<details><summary>Show answer</summary>

With runtime scope, main code that imports a PostgreSQL class directly no longer compiles. The service talks to the database through standard interfaces, and the driver is needed only when the application runs. Runtime scope keeps it on the running classpath and in the jar, while keeping the code independent of one driver.

</details>

2. The build server runs ./mvnw package, and one test fails. What does the build produce, and why?

<details><summary>Show answer</summary>

It produces no new jar, and the build fails. The package phase runs every earlier phase in order, and the test phase comes before it. When a test fails, Maven stops before the jar and repackage goals, so code with a failing test is never packaged. A jar from an earlier build may still sit in target, so a build server should start from a clean folder.

</details>

3. Two starters bring different versions of the same logging library, one at depth 2 of the tree and one at depth 3. Which version does Maven choose, and how can the team force a version?

<details><summary>Show answer</summary>

Maven chooses the version at depth 2, the nearest definition. The team can force a version by declaring the library directly, which puts it at depth 1, or by setting it in dependency management. With the Spring Boot parent, the versions it manages usually settle such conflicts before they appear.

</details>

4. A teammate says Gradle is faster than Maven because the second build took 6 seconds. What did that run actually show?

<details><summary>Show answer</summary>

It showed that Gradle skipped every task because nothing had changed, not that compiling or testing is faster. All 7 actionable tasks were up-to-date. The time also depends on the daemon: the same unchanged build took 1 minute 8 seconds when the daemon had to start again. A fair comparison would change the same file in both projects and time several builds of each, and even then one laptop gives only one data point.

</details>

Reading Progress

0%


Vote for new content

On This Page

Coordinates and Repositories

Dependencies and the Tree They Form

Scopes: Which Classpath Gets a Library

The Maven Lifecycle

Gradle: Tasks Instead of Phases

Maven and Gradle Side by Side

Key Takeaways

Practice Questions