The previous lesson ended with an uncomfortable question: what exactly is mvn test?
You have typed it five times. You have put <scope>test</scope> on a dependency without knowing what it means. You have seen spring-boot-starter-parent, maven-surefire-plugin and spring-boot-maven-plugin appear in a pom.xml and taken them on trust. You have run mvn spring-boot:run and mvn dependency:tree. And in 11-01 there was talk of groupId:artifactId:version coordinates as preparation for this lesson.
This lesson explains the tool that has been holding up the previous three.
Maven is the standard build tool of the Java ecosystem. Its job is to turn your source code and a configuration file into a runnable artifact, resolving the dependencies along the way, running the tests and guaranteeing that the process gives the same result on any machine.
That last point is the most underrated one. Up to module 10, BiblioTech was compiled with javac and a hand-written classpath. With the forty jars Spring Boot has brought in, that is no longer tedious: it is impossible. And there is an underlying reason that goes beyond convenience, and that links back to Log4Shell (11-01): the ability to respond to a vulnerability depends on being able to change a version with one line and verify with one command that nothing broke. You got the second half in 11-04 with JUnit. Here comes the first.
By the end you will understand what problem a build tool solves; you will be able to read and write a complete pom.xml; you will master dependencies, their scopes, transitivity and conflict resolution; you will know the life cycle and its phases; you will know what a plugin is and what the ones you use do; you will handle profiles and multi-module projects; and you will have the judgement to choose between Maven and Gradle.
And BiblioTech will be a complete, reproducible Maven project.
Contents
- The problem:
javacby hand - What a build tool solves
- Convention over configuration
- The standard directory layout
- Installation and verification
- The POM: coordinates
SNAPSHOTversus a released versionproperties: POM variables- BiblioTech's complete
pom.xml - Dependencies: declaration and repositories
- Dependency scopes
- Transitive dependencies
- Conflict resolution: the nearest definition
mvn dependency:treein practiceexclusionsdependencyManagementversusdependencies- The BOM and
spring-boot-starter-parent - The life cycle: three chains
- The phases of the default chain
- Plugins and goals
- The plugins BiblioTech uses
surefireversusfailsafe- Packaging:
maven-jar-plugin,shadeandspring-boot-maven-plugin - Profiles
- Multi-module projects
settings.xmland credentials- The Maven Wrapper
- Day-to-day commands
- Reproducibility and pinning versions
- Maven versus Gradle
- BiblioTech as a complete Maven project
- Common Mistakes and Tips
- Exercises
- The problem:
javac by hand
javac by handLet us go back to module 1. This is how BiblioTech was compiled and run:
javac -d classes src/com/nexussoftware/bibliotech/*.java
java -cp classes com.nexussoftware.bibliotech.MainIt works with ten files and zero dependencies. Now look at what it would take today:
javac -d target/classes \
-cp "lib/spring-core-6.1.11.jar:lib/spring-context-6.1.11.jar:lib/spring-beans-6.1.11.jar:lib/spring-aop-6.1.11.jar:lib/spring-boot-3.3.2.jar:lib/spring-boot-autoconfigure-3.3.2.jar:lib/hibernate-core-6.5.2.jar:lib/jakarta.persistence-api-3.1.0.jar:lib/h2-2.2.224.jar:lib/jackson-databind-2.17.2.jar:lib/slf4j-api-2.0.13.jar:lib/logback-classic-1.5.6.jar:..." \
$(find src/main/java -name "*.java")And that only compiles. Afterwards you would have to:
- Copy the resources from
src/main/resourcesintotarget/classes. - Compile the tests with another classpath (the production one plus JUnit, AssertJ and Mockito).
- Run the tests by invoking JUnit's launcher by hand.
- Package it into a jar with a correct
MANIFEST.MF. - And before any of that, download the forty jars by hand, with their exact, mutually compatible versions.
Point 5 is the killer. Downloading spring-boot-starter-data-jpa means finding out that it needs Hibernate, that Hibernate needs jakarta.persistence-api, ByteBuddy, Jandex, Classmate and Antlr, that ByteBuddy needs... and doing it with versions that do not clash. By hand it is days of work that has to be redone at every update.
It is called dependency hell, and it is the main problem Maven solves.
- What a build tool solves
| Problem | Without a tool | With Maven |
|---|---|---|
| Compiling | javac with a hand-written classpath |
mvn compile |
| Dependencies | Downloading jars by hand | Declaring them in the POM |
| Transitive dependencies | Working them out and downloading one by one | Automatic |
| Version conflicts | Trial and error | A deterministic rule |
| Running tests | Invoking the launcher by hand | mvn test, automatic |
| Packaging | jar cvfm with a hand-written manifest |
mvn package |
| Resources | cp by hand |
Automatic |
| Reproducibility | Depends on the machine | Guaranteed |
| IDE integration | Configuring each one | The IDE reads the POM |
| Continuous integration | A bespoke script | mvn verify |
And a cultural consequence that matters: every Maven project builds the same way. Somebody hands you an unfamiliar repository, you see a pom.xml, you type mvn package and it works. No documentation to read, nobody to ask.
- Convention over configuration
This is Maven's design principle, and it explains why its configuration is so short.
If you follow the conventions, you do not have to configure anything. You only configure what departs from them.
Maven assumes that:
- The source code is in
src/main/java. - The resources are in
src/main/resources. - The tests are in
src/test/java. - The test resources are in
src/test/resources. - Everything generated goes into
target/. - Every
*Test.javafile is a test.
That is why in 11-04 you put the tests in src/test/java and mvn test found them without you configuring anything. It was not magic: it was the convention.
The alternative —explicit configuration, like Ant— requires declaring every path, every task and every dependency between tasks. The result is three-hundred-line build files, different in every project, that you have to read before understanding anything.
The trade-off is real: if your project does not fit the conventions, Maven becomes awkward. It is the price of uniformity, and for the vast majority of Java projects it is worth paying.
- The standard directory layout
bibliotech/
├── pom.xml <- THE configuration file
├── mvnw, mvnw.cmd <- Maven Wrapper (section 27)
├── .mvn/wrapper/ <- wrapper configuration
├── src/
│ ├── main/
│ │ ├── java/ <- production code
│ │ │ └── com/nexussoftware/bibliotech/
│ │ └── resources/ <- application.yml, data.sql, logback.xml
│ └── test/
│ ├── java/ <- tests
│ │ └── com/nexussoftware/bibliotech/
│ └── resources/ <- application-test.yml, test CSVs
└── target/ <- EVERYTHING generated (never in git)
├── classes/ <- production .class files + resources
├── test-classes/ <- test .class files
├── surefire-reports/ <- test reports (11-04)
├── generated-sources/ <- generated code (Lombok, MapStruct)
└── bibliotech-1.0.0-SNAPSHOT.jargraph TD
A["src/main/java"] -->|compile| B["target/classes"]
R["src/main/resources"] -->|process-resources| B
C["src/test/java"] -->|test-compile| D["target/test-classes"]
TR["src/test/resources"] -->|process-test-resources| D
B --> D
D -->|test: surefire| E["target/surefire-reports"]
B -->|package| F["target/bibliotech-1.0.0-SNAPSHOT.jar"]
F -->|install| G["~/.m2/repository"]
Two golden rules:
target/never goes into git. It is all regenerable. Your.gitignoremust containtarget/.src/is never touched by hand from the build. It is the source, and it is the only thing in the repository besides the POM.
- Installation and verification
Apache Maven 3.9.8
Maven home: /opt/maven
Java version: 17.0.11, vendor: Eclipse Adoptium
Default locale: en_GB, platform encoding: UTF-8
OS name: "linux", version: "6.12.0", arch: "amd64"Check three things: Maven's version (3.9.x is the current one), Java's (17 or above for this course) and the platform encoding, which must be UTF-8 to avoid problems with accented characters.
If you do not have it installed, section 27 will give you a very good reason not to need it.
Creating a project from scratch:
mvn archetype:generate \
-DgroupId=com.nexussoftware \
-DartifactId=bibliotech \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=falseIn practice, for a Spring Boot project people use Spring Initializr (start.spring.io), which generates the POM, the wrapper and the main class already configured.
- The POM: coordinates
The POM (Project Object Model) is the pom.xml: the complete description of the project.
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <!-- always 4.0.0 -->
<!-- THE COORDINATES: they identify the artifact uniquely -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Management of Nexus Software's internal technical library</description>
</project>| Coordinate | What it is | Convention |
|---|---|---|
groupId |
The organisation | Reversed domain: com.nexussoftware |
artifactId |
The module | Lowercase with hyphens: bibliotech-core |
version |
The version | SemVer (11-01): 1.0.0 |
packaging |
What is produced | jar (default), war, pom |
The first three form the groupId:artifactId:version coordinates you already saw in 11-01. They determine the path in the repository:
Values for packaging:
| Value | Produces | Use |
|---|---|---|
jar |
A .jar |
Libraries and applications (the normal case) |
war |
A .war |
Web applications for an external server (little used today) |
pom |
Only the POM | Parent or aggregator project (section 25) |
SNAPSHOT versus a released version
SNAPSHOT versus a released versionThe version has two very different natures:
| Type | Example | Meaning | Behaviour |
|---|---|---|---|
| SNAPSHOT | 1.0.0-SNAPSHOT |
Under development | Mutable: Maven re-downloads it periodically |
| Released | 1.0.0 |
Published | Immutable: downloaded once and cached forever |
The practical consequence: if you depend on bibliotech-core:1.0.0-SNAPSHOT, Maven checks daily whether there is a newer version. If you depend on 1.0.0, it downloads it once and never looks again.
Professional rule: a released version is never overwritten. If 1.0.0 has a bug, 1.0.1 is published. Republishing 1.0.0 with different content breaks reproducibility for everybody who already had it cached, and it is one of the worst things you can do in a shared repository.
And its corollary: never depend on a SNAPSHOT in production. It builds today and tomorrow with different content.
Forcing snapshots to update:
properties: POM variables
properties: POM variables<properties>
<java.version>17</java.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- centralised versions, so they are not repeated -->
<assertj.version>3.25.3</assertj.version>
<mockito.version>5.12.0</mockito.version>
</properties>They are used with ${name}:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>The advantage: when fifteen dependencies share a version (the whole Jackson ecosystem, for instance), you change it in one place.
project.build.sourceEncoding is genuinely important: without it, Maven uses the platform encoding, and building on a Spanish Windows machine and on a UTF-8 Linux server can produce different results. Maven emits a WARNING if it is missing.
There are also useful predefined properties: ${project.version}, ${project.artifactId}, ${project.basedir}, ${maven.build.timestamp}.
- BiblioTech's complete
pom.xml
pom.xmlThis is the complete, commented POM, with everything the project needs after four lessons:
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ================= PARENT ================= -->
<!-- Provides the BOM (compatible versions of EVERYTHING) and plugin configuration -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- empty: look for it in the repository, not on disk -->
</parent>
<!-- ================= IDENTITY ================= -->
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BiblioTech</name>
<description>Management of Nexus Software's internal technical library</description>
<!-- ================= PROPERTIES ================= -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- ================= DEPENDENCIES ================= -->
<dependencies>
<!-- Spring core: IoC container, autoconfiguration, logging (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Validation: @NotNull, @Min on @ConfigurationProperties (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- AOP: @Transactional and our own aspects (11-02) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- JPA + Hibernate + Spring Data + transactions (11-03) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2: in-memory database. runtime: not needed to compile -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- @ConfigurationProperties metadata: IDE autocompletion -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing: JUnit 5, AssertJ, Mockito, spring-test (11-04, 11-06) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ================= BUILD ================= -->
<build>
<plugins>
<!-- Packages the executable jar and provides the spring-boot:run goal (11-02) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- UNIT tests: *Test.java -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integration</excludedGroups> <!-- @Tag from 11-04 -->
</configuration>
</plugin>
<!-- INTEGRATION tests: *IT.java, in the verify phase -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- ================= PROFILES ================= -->
<profiles>
<profile>
<id>fast</id> <!-- mvn package -Pfast : no slow tests -->
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>
</project>An important detail: no Spring dependency carries a <version>. The parent manages them. We will come back to that in section 17.
- Dependencies: declaration and repositories
A dependency is declared with its coordinates:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
<scope>compile</scope> <!-- the default; it can be omitted -->
</dependency>Where does the jar come from? Maven looks in three places, in this order:
graph LR
A["mvn compile"] --> B{"Is it in<br/>~/.m2/repository?"}
B -->|Yes| E["It is used"]
B -->|No| C{"Is there a corporate<br/>repository (Nexus,<br/>Artifactory)?"}
C -->|Yes| D["Downloaded from it"]
C -->|No| F["Downloaded from<br/>Maven Central"]
D --> G["Stored in ~/.m2"]
F --> G
G --> E
| Repository | What it is | When |
|---|---|---|
Local (~/.m2/repository) |
A cache on your disk | Always checked first |
| Central | The public repository (11-01) | By default |
| Corporate | Nexus, Artifactory: a mirror plus private artifacts | In a company |
Adding an extra repository (avoid it if you can):
<repositories>
<repository>
<id>nexus-internal</id>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</repository>
</repositories>Professional tip: the fewer repositories, the better. Each one adds a source of artifacts you have to trust. The usual practice in a company is to configure a single corporate mirror in settings.xml (section 26) that in turn replicates Central: that way there is one control point, cached and auditable.
- Dependency scopes
The scope determines which classpath a dependency is available on and whether it is packaged.
| Scope | Compile | Test | Run | Transitive? | Example |
|---|---|---|---|---|---|
compile (default) |
Yes | Yes | Yes | Yes | Spring, Jackson |
provided |
Yes | Yes | No | No | The Servlet API in a .war; Lombok |
runtime |
No | Yes | Yes | Yes | The H2 driver, Logback |
test |
No | Yes | No | No | JUnit, AssertJ, Mockito |
system |
Yes | Yes | No | No | Obsolete, do not use it |
import |
— | — | — | — | Only in dependencyManagement: imports a BOM |
The two you have already used without knowing it:
runtime for H2. Your code never writes import org.h2.Driver. Only the running JVM needs it, when Hibernate loads the driver by name. Putting it in runtime means you cannot use it in your code by accident: if somebody writes a class that imports something from H2, it does not compile. That is a free architectural barrier.
test for JUnit and AssertJ. They are available when compiling and running tests, and they are not packaged into the final jar. That is what keeps your production application from carrying a testing framework inside it — with its weight and its attack surface.
provided deserves a note: it means "at run time this will already be there, do not package it". Its classic case was the Servlet API in a .war, provided by the application server. Today, with embedded servers, it is used mainly for Lombok (11-07), which only acts at compile time.
Seeing the resulting classpath:
- Transitive dependencies
Here is the feature that makes Maven worth it.
When you declare one dependency, Maven also downloads its own, and theirs, recursively.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>That single declaration brings in, among others:
spring-boot-starter-data-jpa
├── spring-boot-starter-aop
│ ├── spring-aop
│ └── aspectjweaver
├── spring-boot-starter-jdbc
│ ├── HikariCP <- connection pool
│ └── spring-jdbc
├── jakarta.persistence-api <- the specification (11-03)
├── jakarta.transaction-api
├── hibernate-core <- the implementation (11-03)
│ ├── byte-buddy <- generates the lazy-loading proxies
│ ├── jandex
│ ├── classmate
│ └── antlr4-runtime <- parses JPQL
├── spring-data-jpa
│ ├── spring-orm
│ └── spring-tx
└── spring-aspectsNearly thirty artifacts you never named. Without Maven, you would have to find each one, work out the compatible version and download it by hand.
And there you can see something 11-03 took for granted: ByteBuddy is in your project because Hibernate uses it to generate the proxy subclasses for lazy loading. It is module 10's mechanism, with a name and coordinates.
An important rule about transitivity: test and provided dependencies do not propagate. If BiblioTech depended on a library that uses JUnit in its tests, you would not inherit JUnit. That is correct: somebody else's test dependencies are none of your business.
- Conflict resolution: the nearest definition
With thirty transitive dependencies, it is inevitable that two of them will ask for different versions of the same thing:
There can only be one version of a class on the classpath. Which one wins?
The nearest-definition rule (nearest definition wins): the version that is fewest hops away from your POM wins. On a tie, the one declared first wins.
your-project (depth 0)
├── library-a (1)
│ └── jackson-databind:2.15.0 (2)
├── jackson-databind:2.17.2 (1) <- WINS: it is nearer
└── library-b (1)
└── jackson-databind:2.16.0 (2)A very useful practical consequence: declaring a dependency explicitly in your POM gives you absolute control over its version, because it will always be at depth 1 and beat any transitive one.
The rule is deterministic, but it can surprise you: "the newest" does not win, "the nearest" does. If a nearby transitive asks for an old version, that old one wins, and you can end up with a NoSuchMethodError at run time — the classic symptom of a version conflict.
Diagnosis:
[INFO] +- com.example:library-a:jar:1.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.15.0:compile
- omitted for conflict with 2.17.2)That omitted for conflict with tells you exactly which version was discarded and in favour of which.
mvn dependency:tree in practice
mvn dependency:tree in practiceIt is the diagnostic tool that will get you out of trouble most often. Four concrete uses:
1. Seeing the whole tree:
2. Finding who drags an artifact in (11-01's security question):
[INFO] com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
[INFO] \- com.example:legacy-library:jar:2.1.0:compile
[INFO] \- org.apache.logging.log4j:log4j-core:jar:2.14.1:compileThere is the answer to "do I have Log4j and who pulled it in?", which in December 2021 cost half the industry weeks of work.
3. Seeing resolved conflicts:
4. Detecting dependencies declared but unused, or used but undeclared:
[WARNING] Used undeclared dependencies found:
[WARNING] org.slf4j:slf4j-api:jar:2.0.13:compile
[WARNING] Unused declared dependencies found:
[WARNING] com.example:utils:jar:1.0.0:compileThat first warning points at a real risk: you are using SLF4J in your code but you get it transitively. If tomorrow Spring Boot stops bringing it in, your code stops compiling without you having touched anything. Whatever you use directly, declare directly.
exclusions
exclusionsSometimes you have to remove something that arrives transitively. Real cases:
<!-- Case 1: replacing Logback with Log4j2 in Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency><!-- Case 2: removing Tomcat in order to use Jetty or Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency><!-- Case 3: removing a duplicate logging implementation that causes a conflict -->
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-library</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>That third case is important and you will see it in 11-07: two logging implementations on the same classpath fight each other and the result is that nothing gets logged, or everything gets logged twice.
To exclude a dependency's entire tree in one go:
Use it carefully: it is easy to remove something that was needed and find out at run time.
dependencyManagement versus dependencies
dependencyManagement versus dependenciesTwo sections that are constantly confused.
| Section | What it does |
|---|---|
<dependencies> |
Adds the dependency to the project |
<dependencyManagement> |
Only declares the version that will be used if somebody adds it |
<dependencyManagement>
<dependencies>
<!-- Does NOT add Jackson. It only says: "if it is used, it will be 2.17.2" -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- THIS does add it. With no version: it takes it from dependencyManagement -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>What it is for, three uses:
- Centralising versions in a multi-module project: the parent declares them, the modules use them without a version.
- Forcing a transitive's version without declaring it as a direct dependency. Very useful for patching a vulnerability: if a transitive has a CVE, here you impose the fixed version.
- Importing a BOM, which is what the next section is about.
Point 2 deserves an example, because it is the rapid-response tool against vulnerabilities:
<dependencyManagement>
<dependencies>
<!-- A transitive brings in 2.14.1, which is vulnerable. We force the fixed one -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.23.1</version>
</dependency>
</dependencies>
</dependencyManagement>dependencyManagement always wins over transitive resolution, regardless of depth. It is the correct way to patch without waiting for the intermediate library to update.
- The BOM and
spring-boot-starter-parent
spring-boot-starter-parentA BOM (Bill of Materials) is a POM of type pom that contains only dependencyManagement: a catalogue of versions tested together.
It is used in two ways.
Way 1: inheriting from the parent (what BiblioTech does):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>That gives you:
| Inherited | What it provides |
|---|---|
dependencyManagement |
Compatible versions of more than 400 artifacts |
pluginManagement |
Versions and configuration of the usual plugins |
| Properties | java.version, encoding, and so on |
| Resource filtering | application.yml with substitutable @property@ |
| Plugin configuration | spring-boot-maven-plugin already hooked to package |
Way 2: importing the BOM (when you already have another parent, typical in a company):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.2</version>
<type>pom</type>
<scope>import</scope> <!-- the import scope from section 11 -->
</dependency>
</dependencies>
</dependencyManagement>And now the warning that appeared in 11-02 makes complete sense:
Do not put a
<version>on dependencies the BOM manages.
If you write <version>2.15.0</version> on Jackson, you are overriding the version the Spring Boot team tested with that version of Spring, of Hibernate and of the rest of the ecosystem. You can end up with subtle incompatibilities: a NoSuchMethodError at run time, six months later, in production.
If you really need another version, the correct way is to override the property:
<properties>
<jackson.version>2.17.2</jackson.version> <!-- name defined by the BOM -->
</properties>
- The life cycle: three chains
Maven has three independent life cycles, each with its own phases:
| Cycle | What for | Main phases |
|---|---|---|
clean |
Cleaning | pre-clean, clean, post-clean |
default |
Building | validate ... deploy (the important one) |
site |
Documentation | pre-site, site, post-site, site-deploy |
mvn clean # runs the clean cycle: deletes target/
mvn package # runs the default cycle up to the package phase
mvn clean package # both, in that ordermvn site generates a website with project reports. It is little used today; most teams use other reporting tools.
- The phases of the default chain
And here is Maven's key idea:
Running a phase runs every previous phase.
That is why mvn test compiled your code in 11-04 without being asked: test comes after compile.
graph TD
A["validate<br/>The project is correct"] --> B["compile<br/>src/main/java -> target/classes"]
B --> C["test<br/>Runs *Test with surefire"]
C --> D["package<br/>Packages into jar/war"]
D --> E["verify<br/>Integration tests (failsafe)<br/>and quality checks"]
E --> F["install<br/>Copies to ~/.m2/repository"]
F --> G["deploy<br/>Uploads to the remote repository"]
The complete list, with the intermediate phases that also exist:
| # | Phase | What it does |
|---|---|---|
| 1 | validate |
Checks that the POM is correct |
| 2 | generate-sources |
Generates source code |
| 3 | process-resources |
Copies src/main/resources to target/classes |
| 4 | compile |
Compiles src/main/java |
| 5 | process-test-resources |
Copies src/test/resources |
| 6 | test-compile |
Compiles src/test/java |
| 7 | test |
Runs the unit tests (surefire) |
| 8 | package |
Packages into a .jar |
| 9 | pre-integration-test |
Prepares the integration environment |
| 10 | integration-test |
Runs the integration tests (failsafe) |
| 11 | post-integration-test |
Tears the environment down |
| 12 | verify |
Checks the integration results |
| 13 | install |
Installs the artifact into ~/.m2/repository |
| 14 | deploy |
Uploads to the remote repository |
Which to use in each situation:
| Situation | Command |
|---|---|
| Development, fast cycle | mvn test |
| Producing the jar | mvn package |
| Continuous integration | mvn verify |
| Publishing for other local modules | mvn install |
| Publishing to the corporate repository | mvn deploy |
A nuance about install: lots of people type mvn clean install out of habit for everything. It is slower than necessary and fills ~/.m2 with local artifacts. To verify, mvn verify is enough; install is only needed when another local project is going to consume your artifact.
- Plugins and goals
And here is the second key idea:
Maven does nothing by itself. Everything is done by plugins.
A phase has no code: it is a hook point. What happens at compile is that the compile goal of the maven-compiler-plugin plugin runs.
| Concept | What it is |
|---|---|
| Plugin | A Maven artifact with runnable tasks |
| Goal | One concrete task: compiler:compile, surefire:test |
| Binding | The association between a goal and a phase |
The default bindings for packaging=jar:
| Phase | Plugin:goal |
|---|---|
process-resources |
maven-resources-plugin:resources |
compile |
maven-compiler-plugin:compile |
test-compile |
maven-compiler-plugin:testCompile |
test |
maven-surefire-plugin:test |
package |
maven-jar-plugin:jar |
install |
maven-install-plugin:install |
deploy |
maven-deploy-plugin:deploy |
A goal can also be invoked directly, with no life cycle, using the plugin:goal syntax:
mvn dependency:tree # dependency plugin, tree goal
mvn spring-boot:run # spring-boot plugin, run goal (11-02)
mvn versions:display-dependency-updates
mvn help:effective-pom # shows the RESULTING POM after inheritanceThat last one is a diagnostic gem: mvn help:effective-pom shows the complete POM after applying the parent's inheritance, with every version and configuration resolved. When you do not understand where a setting comes from, look there.
- The plugins BiblioTech uses
maven-compiler-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release> <!-- better than source+target -->
<parameters>true</parameters> <!-- keeps parameter names -->
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg> <!-- warnings = errors. Strict but healthy -->
</compilerArgs>
</configuration>
</plugin>About <release>17</release> versus <source>/<target>: release is better because on top of setting the language level, it verifies that you only use API that exists in Java 17. With source/target you can compile code using a Java 21 method with JDK 21 and generate bytecode 17 that will fail at run time with NoSuchMethodError. release prevents that at compile time.
About <parameters>true</parameters>: it keeps the parameter names in the bytecode. Spring needs them (for @Value without an explicit name) and so does Jackson (11-07) to map constructors.
spring-boot-maven-plugin
You already used it in 11-02. It provides:
| Goal | What it does |
|---|---|
spring-boot:run |
Runs the application without packaging it |
spring-boot:repackage |
Turns the plain jar into an executable jar (hooked to package) |
spring-boot:build-image |
Builds a container image (12-06) |
maven-surefire-plugin and maven-failsafe-plugin
Both run tests. Their difference is the next section.
surefire versus failsafe
surefire versus failsafe| surefire | failsafe | |
|---|---|---|
| Type of test | Unit | Integration |
| Phase | test |
integration-test + verify |
| Name patterns | *Test.java, Test*.java, *Tests.java |
*IT.java, IT*.java, *ITCase.java |
| If they fail | It stops immediately | Records the failure, runs post-integration-test and fails at verify |
| Speed | Milliseconds | Seconds |
That difference in behaviour on failure is not a whim. An integration test may have started a database or a container in pre-integration-test. If failsafe stopped dead, those resources would be left dangling. That is why failsafe always lets the tear-down run and only fails when it reaches verify.
A practical consequence everybody falls for:
mvn test # runs ONLY the unit tests. The *IT ones are NOT run.
mvn verify # runs the unit tests AND the integration ones.If you write LoanRepositoryIT.java and run mvn test, it does not run and there is no warning at all. Everything looks fine.
Applied to BiblioTech, with the tags from 11-04:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integration,slow</excludedGroups>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<groups>integration</groups>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>That way the fast development cycle (mvn test) takes seconds, and continuous integration (mvn verify) runs everything.
- Packaging:
maven-jar-plugin, shade and spring-boot-maven-plugin
maven-jar-plugin, shade and spring-boot-maven-pluginThree ways to produce a runnable artifact, with different properties.
maven-jar-plugin: the plain jar
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>It produces a jar with only your classes. To run it you have to supply the complete classpath:
It is what you use to publish libraries, where the consumer resolves the dependencies.
maven-shade-plugin: the uber-jar
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.nexussoftware.bibliotech.BiblioTechApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>It unpacks every dependency and puts them mixed together into a single jar. It works, but it has a known problem: if two jars have files with the same name under META-INF/services —very common—, one overwrites the other and something stops working mysteriously. It is solved with transformers, but you have to know about it.
spring-boot-maven-plugin: the layered jar
It is the one BiblioTech uses. You already saw its structure in 11-02: it keeps every dependency as a separate jar inside BOOT-INF/lib/ and provides its own class loader.
| plain jar | shade | Spring Boot | |
|---|---|---|---|
| Dependencies | Outside | Unpacked and mixed | Intact jars inside |
| Resource collisions | — | Yes, a real risk | No |
Runnable with java -jar |
Only with a classpath | Yes | Yes |
| Layers for Docker | No | No | Yes (12-06) |
| Typical use | Libraries | Applications without Spring | Spring Boot applications |
That row about layers matters for 12-06: the Spring Boot jar can be split into layers (dependencies, snapshot dependencies, resources, application classes) so that a Docker image only has to rebuild the layer that changed. Since dependencies change far less than your code, a deployment goes from uploading 50 MB to uploading 200 KB.
exec-maven-plugin
To run any class without Spring Boot:
Useful for utilities and maintenance scripts inside the project.
- Profiles
A profile activates conditional configuration. Not to be confused with the Spring profiles from 11-02: those affect beans at run time; these affect the build.
<profiles>
<profile>
<id>fast</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
<profile>
<id>ci</id>
<activation>
<property><name>env.CI</name></property> <!-- if the CI variable exists -->
</activation>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <!-- coverage, 12-05 -->
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>production</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>Activation:
| Way | Syntax |
|---|---|
| Explicit | mvn package -Pfast |
| Deactivating | mvn package -P!fast |
| By property | <activation><property>... |
| By environment variable | env.CI |
| By operating system | <activation><os><family>windows |
| By JDK version | <activation><jdk>17</jdk> |
| By default | <activeByDefault>true</activeByDefault> |
Seeing which profiles are active:
A warning: do not put business logic into Maven profiles. If the artifact you build differs depending on the profile, you are no longer testing what you deploy. Modern practice is to build a single artifact and configure it at run time with environment variables (11-02, 12-06).
- Multi-module projects
When a project grows, it is split into modules. It is what BiblioTech will need in 12-01.
bibliotech/
├── pom.xml <- aggregator parent, packaging: pom
├── bibliotech-domain/
│ ├── pom.xml
│ └── src/main/java/... <- entities and rules. NO framework dependencies
├── bibliotech-persistence/
│ ├── pom.xml
│ └── src/main/java/... <- JPA repositories. Depends on domain
├── bibliotech-service/
│ ├── pom.xml
│ └── src/main/java/... <- logic. Depends on domain and persistence
└── bibliotech-app/
├── pom.xml
└── src/main/java/... <- @SpringBootApplication. Depends on all of themThe parent POM:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging> <!-- produces NO jar: it aggregates -->
<modules>
<module>bibliotech-domain</module>
<module>bibliotech-persistence</module>
<module>bibliotech-service</module>
<module>bibliotech-app</module>
</modules>
<dependencyManagement>
<dependencies>
<!-- Versions of the internal modules, centralised -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-persistence</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>A child module:
<project>
<parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>bibliotech-persistence</artifactId> <!-- groupId and version inherited -->
<dependencies>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-domain</artifactId> <!-- no version: the parent gives it -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
</project>From the root:
mvn clean install # builds ALL the modules, in dependency order
mvn -pl bibliotech-service install # only that module
mvn -pl bibliotech-service -am install # that module AND the ones it depends onThe real benefit is not organisational, it is architectural: bibliotech-domain cannot use Spring or JPA, because it does not have them on its classpath. Maven enforces the layer separation that in a single-module project depends on the team's discipline. It is a boundary verified by the build, and it is very powerful.
It is developed in 12-01.
settings.xml and credentials
settings.xml and credentialsWhile pom.xml describes the project and goes into git, settings.xml describes your machine and never goes into git.
Locations: ~/.m2/settings.xml (user) and ${maven.home}/conf/settings.xml (global).
<settings>
<!-- Corporate mirror: everything goes through it -->
<mirrors>
<mirror>
<id>nexus-corporate</id>
<mirrorOf>*</mirrorOf>
<url>https://nexus.nexussoftware.com/repository/maven-public/</url>
</mirror>
</mirrors>
<!-- CREDENTIALS: this is why this file does NOT go into the repository -->
<servers>
<server>
<id>nexus-corporate</id>
<username>${env.NEXUS_USERNAME}</username>
<password>${env.NEXUS_PASSWORD}</password>
</server>
</servers>
<proxies>
<proxy>
<id>company-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>proxy.nexussoftware.com</host>
<port>8080</port>
</proxy>
</proxies>
</settings>Security warning. Credentials never go into the
pom.xml, because the POM goes into the code repository and stays in its history forever. They go intosettings.xml, and preferably read from environment variables as in the example. Maven offersmvn --encrypt-passwordto encrypt them with a master key, although that is no substitute for a secrets manager.And a rule that really is absolute: a secret that has been in git is considered compromised forever. Deleting it in a later commit does not remove it from the history. If it happens, the credential must be rotated. Secrets management is covered in 12-07.
- The Maven Wrapper
A real problem: you use Maven 3.9.8, a colleague has 3.6.3, the continuous integration server has 3.8.1. Something fails on only one of the three. Diagnosing it costs a day.
The Maven Wrapper solves it: a script in the repository that downloads and uses the exact version of Maven the project declares.
It generates:
mvnw <- Unix script
mvnw.cmd <- Windows script
.mvn/wrapper/maven-wrapper.properties <- the declared versionAnd from then on:
Advantages:
- Everybody uses the same version, without exception.
- Maven does not need to be installed to build the project. Only Java.
- The version is in git, so changing it is a reviewable commit.
- Continuous integration needs no configuration at all.
These files do go into git (unlike settings.xml). Spring Initializr generates them by default, and it is standard practice today: a modern Java repository builds with ./mvnw verify with nothing installed but a JDK.
- Day-to-day commands
| Command | What it does |
|---|---|
mvn clean |
Deletes target/ |
mvn compile |
Compiles the production code |
mvn test |
Compiles and runs the unit tests |
mvn package |
Produces the jar |
mvn verify |
Everything, including the integration tests |
mvn install |
Installs into ~/.m2 |
mvn clean install |
Rebuilds from scratch and installs |
mvn -DskipTests package |
Packages without running the tests (it compiles them) |
mvn -Dmaven.test.skip=true package |
Does not even compile them |
mvn -o package |
Offline: only the local repository |
mvn -U package |
Forces snapshots to update |
mvn -X package |
Full debug output |
mvn -q package |
Quiet: errors only |
mvn -T 1C package |
Parallel: 1 thread per core |
mvn -pl module -am install |
One module and its dependencies |
mvn dependency:tree |
The dependency tree |
mvn dependency:analyze |
Declared but unused / used but undeclared |
mvn versions:display-dependency-updates |
What is out of date |
mvn versions:display-plugin-updates |
Out-of-date plugins |
mvn help:effective-pom |
The resulting POM after inheritance |
mvn help:active-profiles |
Active profiles |
Three practical notes:
-DskipTestsversus-Dmaven.test.skip=true: the first compiles the tests but does not run them (it catches compilation errors in them); the second does not even compile them. Prefer the first.-o(offline) is useful on a train or with the network down, if you already have everything in~/.m2.-T 1Ccan halve the time of a large multi-module project. It is safe if the plugins you use support parallel execution, and the usual ones do.
And for the periodic update of dependencies:
[INFO] The following dependencies have newer versions:
[INFO] com.h2database:h2 ................................. 2.2.224 -> 2.3.230
[INFO] org.springframework.boot:spring-boot-starter ....... 3.3.2 -> 3.3.3Running it every few weeks and applying the patches is one of the most profitable hygiene practices there is, and the direct answer to the Log4Shell lesson.
- Reproducibility and pinning versions
The same git tag must produce the same artifact, today and three years from now. That is reproducibility, and it is what lets you rebuild an old version to debug an incident.
Things that break it:
| Practice | Why it breaks it | Alternative |
|---|---|---|
<version>LATEST</version> |
It changes over time. Removed in Maven 3 | A concrete version |
<version>RELEASE</version> |
Same. Removed | A concrete version |
<version>[1.0,2.0)</version> (a range) |
Resolves differently depending on the day | A concrete version |
-SNAPSHOT dependencies |
Mutable by definition | A released version |
| Not pinning plugin versions | Maven chooses and it can change | Pin them (or inherit from the BOM) |
| Depending on the platform encoding | Different per machine | project.build.sourceEncoding |
| Depending on the installed JDK version | Different per machine | <release>17</release> |
That second-to-last point is real and always forgotten: if you do not pin a plugin's version, Maven uses the latest available and your build can change behaviour without you having touched anything. spring-boot-starter-parent pins the usual plugins; for the rest, give them a version.
Reproducibility is not theoretical. The day there is an incident in version 1.4.2 that was deployed eight months ago, you will have to rebuild it exactly in order to debug it.
- Maven versus Gradle
The two dominant tools. An honest comparison:
| Aspect | Maven | Gradle |
|---|---|---|
| Configuration | Declarative XML | A DSL in Groovy or Kotlin (imperative) |
| Verbosity | High (XML) | Low |
| Learning curve | Low: fixed conventions | Medium-high: you have to learn the DSL |
| Predictability | Very high: fixed life cycle | Lower: anyone can write logic |
| Incremental compilation | No | Yes |
| Build cache | No | Yes, local and remote |
| Speed on large projects | Lower | Considerably higher |
| Background daemon | No | Yes |
| Flexibility | Limited (plugins) | Total (it is code) |
| Readability by third parties | High: all POMs look alike | Variable: there may be arbitrary logic |
| Adoption in companies | Majority | Growing |
| Android | No | The standard |
| Tooling and documentation | Enormous | Broad |
An example of the same project in both:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Gradle is obviously more concise. And that is also where its risk lies: because the build file is code, a team can end up with conditional logic, custom tasks and behaviour that has to be read and understood before touching anything. A three-hundred-line pom.xml is boring but you understand it at a glance; a hundred-line build.gradle.kts can do anything.
Why this course uses Maven:
- It is the majority in Java enterprise back-end work: it is what you will run into.
- It is easier to learn: you do not have to learn a language on top of the tool.
- It is predictable: the life cycle is always the same, and that is exactly what you want while you are learning everything else.
- Spring Boot's documentation uses Maven in its main examples.
And a professional recommendation: learn Maven first, and Gradle when you need it. The concepts —coordinates, scopes, transitivity, life cycle, plugins— are the same in both. The syntax is what changes.
- BiblioTech as a complete Maven project
The result. Final structure:
bibliotech/
├── .gitignore <- target/, *.iml, .idea/
├── .mvn/wrapper/
│ └── maven-wrapper.properties
├── mvnw, mvnw.cmd
├── pom.xml
└── src/
├── main/
│ ├── java/com/nexussoftware/bibliotech/
│ │ ├── BiblioTechApplication.java
│ │ ├── config/ BiblioTechProperties, BiblioTechConfiguration
│ │ ├── domain/ Material, Book, Magazine, Dvd, Employee, Loan, Reservation
│ │ ├── service/ LoanManager, FineCalculator, NoticeService
│ │ ├── persistence/ MaterialRepository, EmployeeRepository, LoanRepository
│ │ ├── network/ MetadataClient, CatalogServer
│ │ ├── infrastructure/ TimingAspect
│ │ └── presentation/ BiblioTechStartup
│ └── resources/
│ ├── application.yml
│ ├── application-dev.yml
│ ├── application-prod.yml
│ └── data.sql
└── test/
├── java/com/nexussoftware/bibliotech/
│ ├── domain/ LoanTest, MaterialTest, IsbnTest
│ ├── service/ FineCalculatorTest, LoanManagerTest
│ ├── persistence/ AtomicWriteTest, LoanRepositoryIT
│ └── BiblioTechApplicationTests
└── resources/
└── application-test.ymlThe minimum .gitignore:
And the complete cycle:
# Clean build with all the tests
./mvnw clean verify
# Development
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# Package and run
./mvnw clean package
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --spring.profiles.active=prod
# Diagnosis
./mvnw dependency:tree
./mvnw versions:display-dependency-updatesOutput of ./mvnw clean verify:
[INFO] --- clean:3.2.0:clean (default-clean) @ bibliotech ---
[INFO] Deleting /home/marta/bibliotech/target
[INFO] --- resources:3.3.1:resources (default-resources) @ bibliotech ---
[INFO] Copying 4 resources from src/main/resources
[INFO] --- compiler:3.13.0:compile (default-compile) @ bibliotech ---
[INFO] Compiling 31 source files with javac [debug release 17]
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ bibliotech ---
[INFO] Compiling 9 source files with javac [debug release 17]
[INFO] --- surefire:3.2.5:test (default-test) @ bibliotech ---
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.4.1:jar (default-jar) @ bibliotech ---
[INFO] Building jar: /home/marta/bibliotech/target/bibliotech-1.0.0-SNAPSHOT.jar
[INFO] --- spring-boot:3.3.2:repackage (repackage) @ bibliotech ---
[INFO] Replacing main artifact with repackaged archive
[INFO] --- failsafe:3.2.5:integration-test (default) @ bibliotech ---
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- failsafe:3.2.5:verify (default) @ bibliotech ---
[INFO] BUILD SUCCESS
[INFO] Total time: 24.183 sThe whole lesson is in that output: the phases in order, the plugins each one runs, surefire with the 41 unit tests, the jar, Spring Boot's repackage and failsafe with the 3 integration ones.
Compared with the starting point:
| Aspect | javac by hand (modules 1-10) |
Maven |
|---|---|---|
| Compiling | A 15-line command | ./mvnw compile |
| Dependencies | Manually downloading 40 jars | 6 declarations in the POM |
| Transitives | By hand, one by one | Automatic |
| Conflicts | Trial and error | A deterministic rule |
| Tests | There was no convenient way | ./mvnw test |
| Packaging | jar with a hand-written manifest |
./mvnw package |
| Another machine | "It works on mine" | ./mvnw verify, and that is it |
| Changing a version | Download, replace, pray | One line, verify |
| Security audit | Impossible | dependency:tree |
That second-to-last row is the complete answer to Log4Shell: one line to change the version, one command to verify that nothing broke.
- Common Mistakes and Tips
Mistake: putting a <version> on dependencies the BOM manages. You override tested versions and cause subtle incompatibilities that show up at run time. If you need another one, override the property.
Mistake: using version ranges or LATEST. They break reproducibility. Concrete versions, always.
Mistake: believing that mvn test runs the integration tests. The *IT ones are run by failsafe, in mvn verify. And there is no warning that they were not run.
Mistake: not pinning plugin versions. Maven uses the latest available and your build changes without you touching anything.
Mistake: committing target/ to git. It is all regenerable, it takes up a lot of space and it causes constant conflicts.
Mistake: credentials in the pom.xml. The POM goes into git and stays in the history forever. They go into settings.xml, and a secret that has been in git has to be rotated.
Mistake: mvn clean install for everything. It is slower than necessary and it fills ~/.m2. To verify, mvn verify.
Mistake: -DskipTests in continuous integration. That is exactly where they must run.
Mistake: not declaring a dependency you use directly. If it reaches you transitively and tomorrow the intermediary stops bringing it in, your code stops compiling. mvn dependency:analyze detects it.
Mistake: source/target instead of release. It lets you compile code that uses API missing from the target version, and the failure appears at run time.
Mistake: forgetting project.build.sourceEncoding. Accented characters get corrupted differently depending on the machine.
Tip: always use the Maven Wrapper. It eliminates a whole class of "it works on my machine" problems and requires nothing to be installed.
Tip: learn to read dependency:tree. It is what will get you out of trouble most often, and it is the response tool for vulnerabilities.
Tip: mvn help:effective-pom when you do not understand where something comes from. It shows the POM after inheritance, with everything resolved.
Tip: mvn versions:display-dependency-updates every few weeks. Applying patches regularly is far cheaper than an emergency leap.
Tip: keep *Test and *IT apart. The fast cycle takes seconds; continuous integration runs everything.
Tip: mvn -T 1C on large projects. It can halve the time.
Tip: when something fails inexplicably, mvn -X. The debug output says where every setting and every artifact comes from.
- Exercises
Exercise 1: reading and fixing a POM
A colleague at Nexus Software has written this pom.xml for a new microservice. It builds, but it has seven problems. Find them, explain the consequence of each and write the corrected version.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>catalog-service</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
</dependency>
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-common</artifactId>
<version>2.1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>Exercise 2: resolving a dependency conflict
BiblioTech throws this exception at run time, not at compile time:
java.lang.NoSuchMethodError: 'com.fasterxml.jackson.databind.ObjectMapper
com.fasterxml.jackson.databind.json.JsonMapper$Builder.build()'The dependency tree, trimmed:
com.nexussoftware:bibliotech:jar:1.0.0-SNAPSHOT
+- org.springframework.boot:spring-boot-starter-web:jar:3.3.2:compile
| \- org.springframework.boot:spring-boot-starter-json:jar:3.3.2:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
+- com.example:report-client:jar:4.2.0:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
\- com.example:nexus-utils:jar:1.8.0:compile
\- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compileYou are asked for:
- Explain why the error appears at run time and not at compile time.
- Apply the resolution rule: which version wins, and why?
- Give three different solutions with their XML, stating advantages and drawbacks.
- Pick one and justify it.
- Write the command that confirms the conflict has been resolved.
Exercise 3: separating the tests and adding coverage
BiblioTech has 41 unit tests (11-04) and is about to add integration tests against H2. Right now mvn test takes 6 seconds; with the integration ones it would go up to 45.
Configure the pom.xml so that:
mvn testruns only the unit tests (fast, for development).mvn verifyruns unit and integration tests.- The integration ones are named
*IT.javaand tagged with@Tag("integration"). - There is a
ciprofile that also generates the coverage report with JaCoCo and fails if instruction coverage drops below 70 %. - There is a
fastprofile that skips the integration tests even inmvn verify. - Write the commands for each scenario: development, before pushing changes, continuous integration and urgent packaging.
Solutions
Solution 1
The seven problems:
| # | Problem | Consequence |
|---|---|---|
| 1 | <version>3.2.0</version> on spring-boot-starter-data-jpa |
It overrides the parent's BOM (3.3.2). It mixes Spring Data 3.2 with Spring Framework 6.1.11: subtle incompatibilities at run time |
| 2 | <version>LATEST</version> |
Removed in Maven 3; and even if it worked, it completely breaks reproducibility |
| 3 | H2 without <scope>runtime</scope> |
It ends up on the compile classpath: somebody can couple to H2 by accident, and the test database travels to production |
| 4 | junit-jupiter without <scope>test</scope> |
JUnit ends up inside the production jar: unnecessary weight and attack surface |
| 5 | JUnit with an explicit version | The BOM already manages it. And besides, spring-boot-starter-test should be used, which brings AssertJ and Mockito |
| 6 | A -SNAPSHOT dependency in a project with a released version 1.0.0 |
Version 1.0.0 is not reproducible: it depends on a mutable artifact. You never release depending on a SNAPSHOT |
| 7 | <source>/<target> instead of <release> |
It lets you compile code using Java 21 API with JDK 21 and generate bytecode 17: NoSuchMethodError at run time. Besides, the parent already configures it with java.version |
An eighth, minor one: <relativePath/> is missing from <parent>, which makes Maven look for a POM in ../pom.xml first. It does not fail, but it emits a warning and it can surprise you.
Corrected version:
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- (8) look for it in the repository -->
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>catalog-service</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version> <!-- (7) the parent applies it to the compiler -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- (1) no version: the parent's BOM manages it -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- (2) no version: Jackson is managed by the BOM too -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- (3) runtime: it is not used when compiling -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- (4)(5) the test starter: JUnit + AssertJ + Mockito, test scope -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- (6) a RELEASED version of the internal library -->
<dependency>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech-common</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>A note on point 6: if bibliotech-common:2.1.0 is not published yet, the correct solution is to publish it first and then release the service. In the meantime, the service must be 1.0.0-SNAPSHOT. It is the rule that a released version can only depend on released versions.
Solution 2
1. Why at run time and not at compile time.
At compile time, Maven uses the resolved version —2.9.8, as point 2 shows— and the compiler only verifies the signatures your code uses. report-client is already compiled: its bytecode contains a call to JsonMapper$Builder.build() that existed in 2.17 but not in 2.9.8.
At run time the JVM tries to link that call against the class on the classpath, does not find it and throws NoSuchMethodError. Linkage errors happen at run time, not at compile time: it is the same mechanism from module 10 when class loading was discussed.
2. Which version wins.
All three are at depth 2 or 3:
| Path | Depth | Version |
|---|---|---|
starter-web → starter-json → jackson-databind |
3 | 2.17.2 |
report-client → jackson-databind |
2 | 2.9.8 |
nexus-utils → jackson-databind |
2 | 2.13.0 |
report-client (2.9.8) and nexus-utils (2.13.0) tie at depth 2. On a tie, the one declared first in the POM wins, and report-client appears earlier. 2.9.8 wins, the oldest of the three.
That is exactly the case that surprises people: the newest does not win, the nearest does, and an old transitive can bring the project down.
3. Three solutions.
A. Declaring the dependency directly (depth 1: it always wins):
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<!-- no version: the one from Spring Boot's BOM (2.17.2) -->
</dependency>
...
</dependencies>For: simple, explicit, and dependency:analyze stops complaining. Against: you have to remember it is there because of a conflict; a comment is advisable.
B. dependencyManagement (wins at any depth):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>For: it is the tool designed for this, it makes the intent clear and it is the standard way to patch a vulnerable transitive. Against: it pins the version by hand instead of leaving it to the BOM (better: override the <jackson.version> property).
C. exclusions on the offending dependencies:
<dependency>
<groupId>com.example</groupId>
<artifactId>report-client</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- and the same on nexus-utils -->For: explicit about who was causing the problem. Against: verbose, it has to be repeated on every guilty dependency, and if a third one appears tomorrow you have to remember.
4. Which to choose.
B, with one nuance: instead of pinning 2.17.2 by hand, override the BOM's property.
<properties>
<!-- We force Jackson's version: report-client:4.2.0 drags in 2.9.8,
which lacks JsonMapper.Builder.build() (NoSuchMethodError, BIB-207) -->
<jackson.version>2.17.2</jackson.version>
</properties>Reasons: dependencyManagement (which is what lies behind that property) is the mechanism designed for this, it wins at any depth, it works even if a fourth dependency appears tomorrow bringing another version, and the comment documents the why for whoever reads it two years from now.
A mandatory precaution: moving report-client from Jackson 2.9.8 to 2.17.2 is a jump of eight minor versions. It might use API that changed. That is why this change must be accompanied by 11-04's tests running green — which is exactly the argument that lesson closed with.
5. Verification:
[INFO] +- com.example:report-client:jar:4.2.0:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile
- omitted for conflict with 2.17.2)And afterwards, mvn verify to confirm that the 41 tests are still green.
Solution 3
<build>
<plugins>
<!-- (1) SUREFIRE: UNIT tests only -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Double safety: by name and by tag -->
<excludes>
<exclude>**/*IT.java</exclude>
</excludes>
<excludedGroups>integration</excludedGroups>
</configuration>
</plugin>
<!-- (2)(3) FAILSAFE: INTEGRATION tests in verify -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<includes>
<include>**/*IT.java</include>
</includes>
<groups>integration</groups>
<skipITs>${skipITs}</skipITs> <!-- (5) controlled by a profile -->
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skipITs>false</skipITs> <!-- (5) by default they DO run -->
<jacoco.version>0.8.12</jacoco.version>
</properties>
<profiles>
<!-- (4) ci PROFILE: coverage with a threshold -->
<profile>
<id>ci</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<!-- Instruments before the tests -->
<execution>
<id>prepare-agent</id>
<goals><goal>prepare-agent</goal></goals>
</execution>
<!-- Generates the HTML report after the tests -->
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
<!-- Checks the threshold and FAILS if it is not met -->
<execution>
<id>coverage-threshold</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!-- (5) fast PROFILE: no integration tests -->
<profile>
<id>fast</id>
<properties>
<skipITs>true</skipITs>
</properties>
</profile>
</profiles>And the corresponding integration test:
package com.nexussoftware.bibliotech.persistence;
import org.junit.jupiter.api.Tag;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest // (11-04) the persistence layer only
@Tag("integration") // (3) the tag
class LoanRepositoryIT { // (3) IT suffix: failsafe picks it up, not surefire
@Autowired private LoanRepository repository;
// ...
}(6) Commands per scenario:
| Scenario | Command | What it runs | Time |
|---|---|---|---|
| Development, after every change | ./mvnw test |
Only the 41 unit tests | ~6 s |
| Before pushing changes | ./mvnw verify |
Unit + integration | ~45 s |
| Continuous integration | ./mvnw clean verify -Pci |
Everything + coverage with a threshold | ~60 s |
| Urgent packaging | ./mvnw package -Pfast |
Unit tests only, produces the jar | ~10 s |
| A real emergency | ./mvnw package -DskipTests |
No tests at all. Only for debugging locally | ~5 s |
Details of the solution worth underlining:
- A double filter in surefire (by
*ITname and by tag). It is redundant on purpose: if somebody forgets theITsuffix but adds the tag —or the other way round—, the test still stays out of the fast cycle. Redundant filters in build configuration are cheap and they prevent surprises. ${skipITs}as a property instead of duplicating the plugin configuration in the profile. The profile only changes one value.- JaCoCo only in the
ciprofile. Instrumenting the bytecode slows the tests down; in development it adds nothing. checkin theverifyphase. That way the build fails for insufficient coverage after running all the tests, not before.- The 70 % threshold is a team decision, not a universal truth. And remember the warning from 11-04: coverage is not quality, and too high a threshold produces bad tests. It is discussed in depth in 12-05.
Conclusion
BiblioTech is now a complete Maven project, reproducible and runnable on any machine in the world that has Java 17.
You understand what problem a build tool solves: not just compiling, but resolving dependency hell —the thirty transitive artifacts a single starter brings in, with versions that have to be compatible with each other—, running tests, packaging and doing all of it reproducibly. And you know why module 1's javac with a hand-written classpath stopped being viable the moment Spring Boot came in.
You know the principle of convention over configuration, which explains why in 11-04 you put the tests in src/test/java and mvn test found them without you configuring anything, and why any Maven project in the world builds the same way with no documentation to read. With its trade-off acknowledged: if your project does not fit the conventions, Maven becomes awkward.
You can read and write a POM: the groupId:artifactId:version coordinates you already saw in 11-01 and how they map to the path in ~/.m2, the packaging, the difference between a mutable SNAPSHOT and an immutable released version —with its professional rule: a released version is never overwritten and never depends on a SNAPSHOT—, and the properties for centralising versions and pinning the encoding.
You have mastered dependencies: the three repositories they are looked up in, the scopes —including the two you were already using without knowing it, runtime for H2 and test for JUnit, which is what keeps a testing framework out of the production jar—, the transitivity that brings in thirty jars from one declaration, and the nearest-definition rule with its consequence that surprises everybody: the newest does not win, the nearest does, and an old transitive can cause a NoSuchMethodError at run time. You know how to diagnose it with dependency:tree —which also answers 11-01's security question: "do I have this library and who pulled it in?"— and with dependency:analyze, which detects the real risk of directly using something you only have transitively.
You can tell dependencyManagement from dependencies and you know their three uses, including the one that matters for security: forcing the fixed version of a vulnerable transitive without waiting for the intermediary to update. And you finally understand spring-boot-starter-parent completely, as a BOM managing more than 400 artifacts, with the warning you had been carrying since 11-02 now justified: putting a <version> on what the BOM manages means overriding versions tested together.
You know the life cycle with its three chains and the key idea that running a phase runs every previous one —which is the answer to why mvn test compiled your code without being asked—, and the second key idea: Maven does nothing by itself, everything is done by plugins hooked to phases, also invocable directly as plugin:goal. You know what BiblioTech's plugins do, why <release>17</release> is better than source/target, and —crucially— the difference between surefire and failsafe, with the trap everybody falls into: mvn test does not run the *IT ones, and it does not warn you.
You know how to package in three ways and why Spring Boot's layered jar is superior to shade's uber-jar, with its consequence for 12-06: rebuilding only the layer that changed turns a 50 MB deployment into a 200 KB one. You handle build profiles —different from Spring's— with the warning not to build different artifacts per environment. And you know about multi-module projects, whose real benefit is not organisational but architectural: bibliotech-domain cannot use Spring because it does not have it on its classpath, and that turns layer separation into a boundary verified by the build.
You know that credentials go into settings.xml, never into the POM, and that a secret that has been in git is considered compromised forever. You use the Maven Wrapper, which eliminates a whole class of "it works on my machine" problems and lets the project be built without installing Maven. You have the day-to-day commands and the reproducibility practices: never ranges, never LATEST, never a SNAPSHOT in production, pinned plugin versions and explicit encoding.
And you have the judgement for the comparison with Gradle: more concise, with caching and incremental builds, considerably faster on large projects; but less predictable, because its build file is code that can do anything. With the practical recommendation: the concepts transfer; learn Maven first.
BiblioTech builds with ./mvnw clean verify in twenty-four seconds: forty-one unit tests, three integration tests, and a fifty-megabyte executable jar that starts on any machine with Java 17. And changing the version of a vulnerable dependency is now one line and one command. The Log4Shell lesson is settled.
But look at those forty-one tests again.
They test FineCalculator, which only needs a Clock. They test Loan's rules, which are plain Java. They test AtomicWrite with a temporary directory. And as soon as a real collaborator appeared —a repository, a notice service—, you had to write by hand an in-memory implementation and another one that recorded calls: twenty lines of inner classes per test, with two simple collaborators. LoanManager has five. LoanRepository is a Spring Data interface with twenty inherited methods: implementing it by hand is unfeasible. And the MetadataClient from 09-06 needs a real HTTP API at the other end.
There are also questions that assertions about the result do not answer. Was the notice sent to the right employee, with the right text? Was the repository called exactly once or forty times? What happens when the database throws an exception —a scenario that is extremely hard to provoke with a real implementation and that in production will happen on any given Tuesday?
The next lesson brings in test doubles by name —dummy, stub, spy, mock and fake— and Mockito 5, which generates them for you with an annotation, using exactly module 10's bytecode generation. You will define behaviour with when(...).thenReturn(...), provoke failures with thenThrow, verify interactions with verify —and learn the criterion for when to verify and when not to, because too much verification produces precisely those brittle tests that break when you refactor—, capture with ArgumentCaptor what was really passed to the notice service, and test the MetadataClient without touching the network.
And you will see something more important than any API: that sometimes the best answer is not a mock, but a design that makes the mock unnecessary. Just as 11-04's fixed Clock did.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
