Building & Packaging with Maven
Building & Packaging with Maven
Understanding the Maven lifecycle and its plugin ecosystem is what separates developers who merely use Maven from those who truly control their build. In this lesson you will learn precisely what happens when you run mvn package, how to produce executable JARs and "fat" (uber) JARs, and how to tune plugins for real-world deployment scenarios.
The Maven Build Lifecycle in Detail
Maven defines three built-in lifecycles: default (compilation through deployment), clean (removes build output), and site (generates documentation). Almost all day-to-day work lives in the default lifecycle. Its phases execute in strict order — invoking a later phase always runs every phase before it:
validate— checks the POM and project structure are correct.compile— compilessrc/main/javatotarget/classes.test-compile— compilessrc/test/javatotarget/test-classes.test— executes unit tests via Surefire; build fails if tests fail.package— bundles compiled classes into a JAR (or WAR/EAR).verify— runs integration tests and other quality checks.install— copies the artifact into your local~/.m2repository.deploy— uploads the artifact to a remote repository (Nexus, Artifactory, etc.).
compiler:compile). When a phase runs, Maven executes every goal bound to it. You can also invoke a goal directly: mvn compiler:compile.
Producing a Standard JAR
The maven-jar-plugin is bound to the package phase by default and needs no explicit configuration for simple projects. Running mvn package produces target/my-app-1.0.jar containing compiled classes and resources. However, this JAR has no Main-Class entry in its manifest and cannot be executed with java -jar directly.
To make the JAR executable, configure the plugin in your POM:
With addClasspath enabled, Maven writes a Class-Path entry in the manifest pointing to a lib/ directory alongside the JAR. You then ship your dependencies in that directory. This is lightweight but fragile for distribution — the relative paths must be preserved.
Producing an Executable Uber-JAR with the Shade Plugin
An uber-JAR (also called a fat JAR) bundles your code and all dependency JARs into a single self-contained archive. The maven-shade-plugin is the standard Maven tool for this:
After mvn package, the target/ directory will contain both my-app-1.0.jar (thin) and my-app-1.0-exec.jar (shaded, runnable). Execute it with:
maven-shade-plugin when you need a runnable uber-JAR. Use maven-assembly-plugin when you need a custom distribution archive (zip/tar with scripts, config files, etc.). For Spring Boot applications, use the dedicated Spring Boot Maven Plugin — it produces a layered JAR optimised for Docker caching.
Controlling the Compiler Plugin
The maven-compiler-plugin is bound to both compile and test-compile. For Java 17+ projects, configure it explicitly — do not rely on Maven's default target version (Java 5 in older Maven installations):
The release flag (Java 9+) is preferred over the older source/target pair because it also sets the bootstrap classpath, preventing accidental use of APIs not present in that Java version.
Skipping and Running Tests Selectively
The Surefire plugin runs unit tests during the test phase. You will sometimes need to skip tests or filter them:
-DskipTests in CI/CD pipelines. The point of automated builds is to catch regressions. Only skip tests locally when iterating rapidly on non-test code, and always run the full suite before pushing.
The Clean Lifecycle
The clean lifecycle has a single meaningful phase: clean, which deletes the target/ directory. Combine it with build phases to guarantee a fresh build:
Always run clean in CI and before releasing. Stale compiled classes from a previous run can produce misleading "it works on my machine" failures.
Binding Custom Goals to Phases
Any plugin goal can be bound to any lifecycle phase. A common pattern is running the Checkstyle or SpotBugs analysis during verify:
Now mvn verify compiles, tests, packages, and then performs static analysis — all in one command, without remembering separate invocations.
Reading Build Output
Maven prints the phase and goal being executed on each line. Learn to read the output pattern:
A BUILD FAILURE line is always accompanied by the specific plugin and goal that failed. Start your debugging there, not at the bottom of a wall of stack traces.
Summary
The Maven default lifecycle flows from validate through deploy; invoking any phase runs all prior phases automatically. The maven-jar-plugin produces a thin JAR; the maven-shade-plugin produces a self-contained executable uber-JAR. Configure the compiler plugin to set the Java release version explicitly. Bind static-analysis goals to verify so quality gates run automatically. Always include clean in CI builds to eliminate stale artefacts.