Appearance
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 compileMaven 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 packageGroup 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:
| Scope | Available | Typical use |
|---|---|---|
compile (default) | compiling, testing, running | most libraries |
test | only for tests | JUnit, Mockito |
provided | compiling; the runtime supplies it | servlet API inside an external Tomcat |
runtime | running, not compiling | a 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:
| Phase | Does |
|---|---|
| validate | checks the project is correct |
| compile | compiles src/main/java into target/classes |
| test | compiles and runs the tests in src/test/java |
| package | bundles the compiled code into a JAR (or WAR) in target/ |
| verify | checks on the package (integration tests, analysis) |
| install | copies the package into your local repository |
| deploy | uploads the package to a remote repository |
Two rules:
- Phases are sequential. Running one phase first runs every phase before it.
mvn packageruns validate, compile, test, then package. - A phase is made of goals. A goal is one task, provided by a plugin (the compile phase runs the compiler plugin's
compilegoal). 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 SUCCESSNothing 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 SUCCESSEach --- 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 SUCCESSThe 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 SUCCESSThe 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 SUCCESSInstall 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 parameterMaven 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
- What is the difference between telling Ant and telling Maven to compile?
- Where does Maven look for configuration when you run a command?
- What is the parent POM, and what does a POM without a
<parent>inherit from? - Why does your
pom.xmlcontain no<repositories>block, yet dependencies still download? - What is the difference between the
testandruntimescopes? - You run
mvn package. Which phases run, in which order? - What is a goal, and how do you add your own goal to the validate phase?
- What does the
spring-boot-maven-pluginadd in the package phase? - What is the difference between
mvn installandmvn deploy? - In which order does Maven look for a dependency, and why does the first build take longest?
mvn deployfails with "repository element was not specified in the POM". What is missing, and where do the credentials go?
Answers
- With Ant you say what and how (create folders, run
javacon these paths). With Maven you say only what (mvn compile); its plugins know how. - In
pom.xmlin the current directory (plus everything it inherits). - A POM whose configuration your project inherits, here
spring-boot-starter-parent. Without a<parent>, the Super POM. - Maven Central is declared in the Super POM, which every POM inherits.
test: only for compiling and running tests.runtime: not needed to compile, but needed when the application runs (for example a database driver).- validate, compile, test, package.
- A single task, provided by a plugin. Add the plugin under
<build><plugins>with an<execution>that names<phase>validate</phase>and the goal. - It repackages the JAR into an executable JAR with all dependencies (Tomcat included), runnable with
java -jar. - Install copies the JAR into your local repository (
~/.m2); deploy uploads it to a remote repository. - Local repository first, then the remote one, saving a local copy. The first build has to download everything; later builds find it locally.
- A
<distributionManagement>block saying where to deploy. The credentials go in~/.m2/settings.xmlunder<servers>, with the sameid.