Build Tools & Modules

Building & Packaging with Maven

15 min Lesson 4 of 13

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:

  1. validate — checks the POM and project structure are correct.
  2. compile — compiles src/main/java to target/classes.
  3. test-compile — compiles src/test/java to target/test-classes.
  4. test — executes unit tests via Surefire; build fails if tests fail.
  5. package — bundles compiled classes into a JAR (or WAR/EAR).
  6. verify — runs integration tests and other quality checks.
  7. install — copies the artifact into your local ~/.m2 repository.
  8. deploy — uploads the artifact to a remote repository (Nexus, Artifactory, etc.).
Phases vs. Goals: A phase is a step in the lifecycle. A goal is a specific action provided by a plugin (e.g., 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:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>com.example.App</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin> </plugins> </build>

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:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation= "org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.App</mainClass> </transformer> <!-- merge META-INF/services files from all JARs --> <transformer implementation= "org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> <!-- create a separate shaded artifact; keep the thin JAR too --> <shadedArtifactAttached>true</shadedArtifactAttached> <shadedClassifierName>exec</shadedClassifierName> </configuration> </execution> </executions> </plugin>

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:

java -jar target/my-app-1.0-exec.jar
When to use the Shade plugin vs. the Assembly plugin: Use 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):

<properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>

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:

# skip test execution (still compiles test sources) mvn package -DskipTests # skip compilation AND execution (faster, use with care) mvn package -Dmaven.test.skip=true # run a single test class mvn test -Dtest=OrderServiceTest # run a single test method mvn test -Dtest=OrderServiceTest#calculateTotal
Never use -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:

mvn clean package mvn clean install mvn clean verify

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:

<plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> <version>4.8.3.0</version> <executions> <execution> <id>spotbugs-check</id> <phase>verify</phase> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin>

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:

[INFO] --- maven-compiler-plugin:3.11.0:compile (default-compile) --- [INFO] --- maven-jar-plugin:3.3.0:jar (default-jar) --- [INFO] Building jar: /project/target/my-app-1.0.jar [INFO] BUILD SUCCESS

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.