Skip to content
Chapter 04 · Spring Boot

Maven and Its Build Lifecycle ​

The project from chapter 3 was generated as a Maven project, and Maven is what downloaded every library behind the two starters. Maven is easy to underestimate as "the thing that builds the JAR". This chapter shows what it really does, by reading the project's pom.xml and then running each build phase on that project.

What Maven is ​

Maven is a project management tool, not only a build tool. It helps with:

  • build generation: compiling, testing and packaging into a JAR or WAR;
  • dependency resolution: downloading the libraries the project needs, and the libraries those need;
  • documentation and reports, a standard project structure, and more.

Everything is driven by one file, pom.xml (Project Object Model). When you run a Maven command, Maven first looks for pom.xml in the current directory and reads what to do from it.

What, not how ​

Before Maven, the popular build tool was Ant. With Ant you described what to do and how, step by step. Compiling looked like this:

xml
<target name="compile">
    <mkdir dir="build/classes"/>
    <javac srcdir="src" destdir="build/classes"/>   <!-- which tool, which folders -->
</target>

With Maven you only say what:

bash
mvn compile

Maven already knows how: which folder holds the sources, that javac compiles them, and where the classes go. That knowledge lives in plugins, which Maven downloads when it needs them.

The project structure ​

Maven expects a fixed layout, which is why every Spring Boot project from Initializr looks the same:

text
learningspringboot/
├── pom.xml
└── src/
    ├── main/java/com/example/learningspringboot/
    │   └── LearningspringbootApplication.java          your code
    └── test/java/com/example/learningspringboot/
        └── LearningspringbootApplicationTests.java     its tests, same package

Group and artifact from Initializr became the package com.example.learningspringboot. Tests mirror the main code's packages under src/test/java. Because the layout is a convention, Maven needs no configuration to find either.

Reading pom.xml ​

Here is the pom.xml Initializr generated, section by section.

1. The schema ​

xml
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

The schema defines which elements are allowed and where. Inside <parent>, for example, only elements such as groupId, artifactId and version are valid; an invented <xyz> element there is rejected.

2. The parent ​

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.5</version>
    <relativePath/>
</parent>

Every POM can have a parent, and inherits its configuration: dependency versions, plugin settings, defaults. Your project is a child of spring-boot-starter-parent, which is why your own pom.xml is so short: most of the configuration is in the parent.

A POM without a <parent> still inherits from Maven's built-in Super POM, which sits at the top of every hierarchy and holds Maven's defaults (for example the Maven Central repository and the default plugins).

3. The coordinates ​

xml
<groupId>com.example</groupId>
<artifactId>learningspringboot</artifactId>
<version>0.0.1-SNAPSHOT</version>

Group, artifact and version identify a project uniquely; they are how any project in the world can depend on yours. SNAPSHOT marks a version still in development.

4. Properties ​

xml
<properties>
    <java.version>21</java.version>
</properties>

Key-value pairs that can be referenced anywhere in the POM as ${key}. Writing <version>${java.version}</version> somewhere would be replaced with 21. (The Spring Boot parent reads java.version to configure the compiler.)

5. Repositories ​

xml
<repositories>
    <repository>
        <id>central</id>
        <url>https://repo.maven.apache.org/maven2</url>
    </repository>
</repositories>

Where Maven downloads dependencies from. Your generated POM does not contain this block, because Maven Central is already declared in the Super POM. You add a <repositories> block only for another repository, such as your company's.

6. Dependencies ​

xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

The libraries your code uses, without versions because the parent manages them. The scope says when a dependency is available:

ScopeAvailableTypical use
compile (default)compiling, testing, runningmost libraries
testonly for testsJUnit, Mockito
providedcompiling; the runtime supplies itservlet API inside an external Tomcat
runtimerunning, not compilinga database driver

7. Build ​

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

The <build> section adds or configures plugins, which do the actual work of each build phase. To understand it, you need the lifecycle first.

The build lifecycle ​

A build runs through phases, always in this order:

PhaseDoes
validatechecks the project is correct
compilecompiles src/main/java into target/classes
testcompiles and runs the tests in src/test/java
packagebundles the compiled code into a JAR (or WAR) in target/
verifychecks on the package (integration tests, analysis)
installcopies the package into your local repository
deployuploads the package to a remote repository

Two rules:

  1. Phases are sequential. Running one phase first runs every phase before it. mvn package runs validate, compile, test, then package.
  2. A phase is made of goals. A goal is one task, provided by a plugin (the compile phase runs the compiler plugin's compile goal). Running a goal also runs everything before it.

(The full lifecycle has 23 phases, with smaller ones in between such as process-resources and test-compile. These seven are the ones you call and attach work to.)

Now run each phase on the project from chapter 3.

Phase 1: mvn validate ​

text
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] BUILD SUCCESS

Nothing visible happens: by default no goal is attached to validate, and Maven only checks that the POM itself is valid. Maven does not impose rules on how your code looks.

Adding your own goal to a phase. If your company enforces a code style, attach the Checkstyle plugin to validate, so a badly formatted project fails before anything compiles:

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-checkstyle-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <id>validate-style</id>
                    <phase>validate</phase>             <!-- which phase -->
                    <goals>
                        <goal>check</goal>              <!-- which goal of the plugin -->
                    </goals>
                    <configuration>
                        <configLocation>my_code_style.xml</configLocation>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

The plugin's coordinates tell Maven where to download it; <phase> and <goal> say which of its tasks runs, and when. The same pattern adds a goal to any phase.

Phase 2: mvn compile ​

text
[INFO] --- resources:3.3.1:resources (default-resources) @ demo ---
[INFO] --- compiler:3.13.0:compile (default-compile) @ demo ---
[INFO] Compiling 8 source files with javac [debug parameters release 21] to target/classes
[INFO] BUILD SUCCESS

Each --- plugin:version:goal --- line is a goal running. You never mentioned javac; the compiler plugin, configured in the parent POMs, ran it. A new folder appears:

text
target/
└── classes/com/example/demo/
    ├── DemoApplication.class
    ├── controller/PaymentController.class
    └── ...

Phase 3: mvn test ​

Add a test like the one in src/test/java:

java
class DemoApplicationTests {

    @Test
    void sampleTest() {
        System.out.println("test case running");
    }
}
text
[INFO] --- compiler:3.13.0:compile (default-compile) @ demo ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ demo ---
[INFO] --- surefire:3.2.5:test (default-test) @ demo ---
test case running
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

The sequence shows rule 1: compile ran first (and found nothing new to compile), then the test sources were compiled, then the Surefire plugin ran the tests. Surefire runs classes named Test*, *Test, *Tests or *TestCase. A failing test stops the build here.

Phase 4: mvn package ​

text
[INFO] --- surefire:3.2.5:test (default-test) @ demo ---
[INFO] --- jar:3.4.2:jar (default-jar) @ demo ---
[INFO] Building jar: .../target/demo-0.0.1-SNAPSHOT.jar
[INFO] --- spring-boot:3.3.5:repackage (repackage) @ demo ---
[INFO] Replacing main artifact .../target/demo-0.0.1-SNAPSHOT.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] BUILD SUCCESS

The JAR plugin builds target/demo-0.0.1-SNAPSHOT.jar. Then the spring-boot-maven-plugin from <build> repackages it into an executable ("fat") JAR containing all dependencies, Tomcat included. That is why java -jar target/demo-0.0.1-SNAPSHOT.jar starts the whole application, and why the <build> section matters.

Phase 5: mvn verify ​

Like validate, verify has no goal of its own here. It is the place for checks on the finished package: integration tests, or static code analysis. The PMD plugin, for example, reports unused variables and imports, empty catch blocks and duplicated code:

xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-pmd-plugin</artifactId>
    <version>3.26.0</version>
    <executions>
        <execution>
            <id>static-analysis</id>
            <phase>verify</phase>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

mvn verify then runs validate, compile, test, package, and finally PMD's check.

Phase 6: mvn install ​

text
[INFO] --- install:3.1.3:install (default-install) @ demo ---
[INFO] Installing .../pom.xml to ~/.m2/repository/com/example/demo/0.0.1-SNAPSHOT/demo-0.0.1-SNAPSHOT.pom
[INFO] Installing .../target/demo-0.0.1-SNAPSHOT.jar to ~/.m2/repository/com/example/demo/0.0.1-SNAPSHOT/demo-0.0.1-SNAPSHOT.jar
[INFO] BUILD SUCCESS

Install copies the JAR into your local repository, a folder on your own machine, under a path built from the coordinates (com/example/demo/0.0.1-SNAPSHOT/). Other projects on your machine can now depend on it.

Local and remote repositories ​

  • Local repository: on your machine, by default ~/.m2/repository.
  • Remote repository: outside your machine. Maven Central (public), or your company's own repository (such as Nexus or Artifactory) inside its network.

When Maven resolves a dependency, it checks the local repository first. Only if the dependency is missing does it download it from the remote one, and then it stores a copy locally. The next build finds it locally and skips the network. That is why the first build of a project is slow and the next ones are fast.

The local repository's location is set in ~/.m2/settings.xml:

xml
<settings>
    <localRepository>/home/me/my-maven-repo</localRepository>
</settings>

Phase 7: mvn deploy ​

Deploy uploads the package to a remote repository, so other people can depend on it. On the project as generated, it fails:

text
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.1.3:deploy (default-deploy) on project demo:
Deployment failed: repository element was not specified in the POM inside distributionManagement element
or in -DaltDeploymentRepository=id::url parameter

Maven has no default place to deploy to: you must say where, in pom.xml:

xml
<distributionManagement>
    <repository>
        <id>company-releases</id>
        <url>https://nexus.mycompany.com/repository/maven-releases/</url>
    </repository>
</distributionManagement>

Uploading needs credentials. They go in ~/.m2/settings.xml, never in pom.xml (which is committed to source control), matched by the same id:

xml
<settings>
    <servers>
        <server>
            <id>company-releases</id>
            <username>my-username</username>
            <password>my-password</password>
        </server>
    </servers>
</settings>

In a company, the POM usually already has distributionManagement pointing at the company's repository, and the build server holds the credentials.

Everyday commands ​

bash
mvn clean            # delete target/
mvn package          # build the executable JAR (runs the tests)
mvn clean install    # fresh build, then install into ~/.m2
mvn spring-boot:run  # run the application from the source (a plugin goal, not a phase)

clean belongs to a separate, smaller lifecycle; combining mvn clean install makes sure nothing stale from an earlier build is packaged.

Next: The project builds and runs. Chapter 5 starts writing real APIs: the controller annotations that map HTTP requests to methods and read path variables, query parameters and request bodies.

Check yourself ​

  1. What is the difference between telling Ant and telling Maven to compile?
  2. Where does Maven look for configuration when you run a command?
  3. What is the parent POM, and what does a POM without a <parent> inherit from?
  4. Why does your pom.xml contain no <repositories> block, yet dependencies still download?
  5. What is the difference between the test and runtime scopes?
  6. You run mvn package. Which phases run, in which order?
  7. What is a goal, and how do you add your own goal to the validate phase?
  8. What does the spring-boot-maven-plugin add in the package phase?
  9. What is the difference between mvn install and mvn deploy?
  10. In which order does Maven look for a dependency, and why does the first build take longest?
  11. mvn deploy fails with "repository element was not specified in the POM". What is missing, and where do the credentials go?
Answers
  1. With Ant you say what and how (create folders, run javac on these paths). With Maven you say only what (mvn compile); its plugins know how.
  2. In pom.xml in the current directory (plus everything it inherits).
  3. A POM whose configuration your project inherits, here spring-boot-starter-parent. Without a <parent>, the Super POM.
  4. Maven Central is declared in the Super POM, which every POM inherits.
  5. test: only for compiling and running tests. runtime: not needed to compile, but needed when the application runs (for example a database driver).
  6. validate, compile, test, package.
  7. A single task, provided by a plugin. Add the plugin under <build><plugins> with an <execution> that names <phase>validate</phase> and the goal.
  8. It repackages the JAR into an executable JAR with all dependencies (Tomcat included), runnable with java -jar.
  9. Install copies the JAR into your local repository (~/.m2); deploy uploads it to a remote repository.
  10. Local repository first, then the remote one, saving a local copy. The first build has to download everything; later builds find it locally.
  11. A <distributionManagement> block saying where to deploy. The credentials go in ~/.m2/settings.xml under <servers>, with the same id.