Maven Plugins, Lifecycle Bindings, Plugin Configuration, Executions, and Plugin Management: Guided Hands-On Workflow and Core Operations
Build a disposable parent/child Maven project, pin plugin versions centrally, inspect goal metadata and the effective POM, prove that pluginManagement alone does not activate a custom execution, then activate a named execution and verify its artifact effect.
Learning objectives
- Create a disposable Maven parent/child project using wrappers and an isolated local repository.
-
Pin core and custom build-plugin versions in parent
pluginManagement. - Inspect plugin goal metadata and the child effective POM before changing activation.
-
Prove that a custom AntRun execution under
pluginManagementis inactive until referenced by the child. -
Activate a named execution bound to
prepare-packageand verify the generated marker enters the packaged JAR.
-Dmaven.repo.local=<lab>/.lab-m2/repository.
Normal user caches/settings are not deleted or rewritten.
./mvnw, grep, find, and
sha256sum. On Windows use mvnw.cmd and
PowerShell equivalents such as Select-String,
Get-ChildItem, and Get-FileHash. The POM
model, plugin execution identity, and lifecycle ordering are
platform-independent; shell syntax is not.
1. Scenario — one parent policy, one application module
Create maven-plugin-lab/ as disposable state. The root
is both an aggregator and parent POM. The child
app inherits plugin versions/configuration. We use
AntRun only as a small visible teaching execution; production code
generation should normally prefer purpose-built plugins or ordinary
application code rather than embedding large Ant scripts.
maven-plugin-lab/
├── pom.xml
├── mvnw
├── mvnw.cmd
├── .mvn/wrapper/...
├── .lab-m2/repository/ # disposable isolated local repository
└── app/
├── pom.xml
└── src/
├── main/java/dev/academy/App.java
└── test/java/dev/academy/AppTest.java
2. Preflight — prove tool and wrapper identity first
Reuse the checked wrapper pattern from Chapter 04. If this standalone lab does not yet contain a wrapper, bootstrap it once with a trusted system Maven and the pinned Wrapper Plugin, then review the generated wrapper properties before using it. Do not fetch wrapper scripts from an arbitrary gist.
cd maven-plugin-lab
REPO="$PWD/.lab-m2/repository"
mkdir -p "$REPO" evidence
./mvnw --version
java -version
cat .mvn/wrapper/maven-wrapper.properties
# Optional one-time bootstrap only if mvnw/mvnw.cmd do not exist:
# mvn org.apache.maven.plugins:maven-wrapper-plugin:3.3.4:wrapper # -Dmaven=3.9.16 -Dtype=only-script
Record the Maven home/distribution and Java runtime printed by
--version. The JDK that runs Maven is not automatically
the same concept as the Java release target used by Compiler Plugin.
3. Author the parent POM — central policy, not magic activation
The parent pins reviewed plugin versions and defines the AntRun
execution. The execution writes a deterministic marker into
${project.build.outputDirectory}, which is normally
target/classes. Because
prepare-package precedes package, the
later JAR goal can include that marker.
<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>
<groupId>dev.academy</groupId>
<artifactId>plugin-lab-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>app</module>
</modules>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.13.4</junit.version>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<archive>
<manifestEntries>
<Build-Policy>academy-plugin-lab</Build-Policy>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>write-build-marker</id>
<phase>prepare-package</phase>
<goals><goal>run</goal></goals>
<configuration>
<target>
<echo file="${project.build.outputDirectory}/build-marker.txt">phase=prepare-package</echo>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
4. Add a tiny application and test
The source is deliberately ordinary. The point is to make plugin effects visible around a known-good Java project.
package dev.academy;
public final class App {
private App() {}
public static String message(String name) {
String safe = (name == null || name.isBlank()) ? "learner" : name;
return "Hello, " + safe + "!";
}
public static void main(String[] args) {
System.out.println(message(args.length == 0 ? null : args[0]));
}
}
package dev.academy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class AppTest {
@Test
void formatsGreeting() {
assertEquals("Hello, Maven!", App.message("Maven"));
}
}
5. First child model — AntRun managed, but not active
Start with the core build plugins referenced in
<plugins>, while AntRun exists only in the parent
pluginManagement. The child omits plugin versions
because those are inherited from central policy.
<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>dev.academy</groupId>
<artifactId>plugin-lab-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><artifactId>maven-resources-plugin</artifactId></plugin>
<plugin><artifactId>maven-compiler-plugin</artifactId></plugin>
<plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
<plugin><artifactId>maven-jar-plugin</artifactId></plugin>
<!-- AntRun is managed by the parent, but is NOT active yet. -->
</plugins>
</build>
</project>
Before building, predict: Compiler/Surefire/JAR/Resources are active because the child references them (and some also have packaging defaults). AntRun's managed execution should appear as policy in the parent model but should not run for the child because AntRun is not activated.
6. Inspect effective plugin configuration and goal help
Use fully qualified Help/AntRun coordinates so the inspection itself has explicit plugin identity.
REPO="$PWD/.lab-m2/repository"
./mvnw -Dmaven.repo.local="$REPO" org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -pl app -Doutput=evidence/effective-before.xml
./mvnw -Dmaven.repo.local="$REPO" org.apache.maven.plugins:maven-antrun-plugin:3.2.0:help -Ddetail=true -Dgoal=run
grep -n -A18 -B4 'maven-antrun-plugin' evidence/effective-before.xml || true
The goal-help output describes parameters/system requirements for
antrun:run. The effective model may show managed
configuration, but model presence alone is not execution evidence.
7. Build once and prove the managed custom execution did not run
Use clean verify to remove stale
target/ output before interpreting the marker.
set -o pipefail
./mvnw -Dmaven.repo.local="$REPO" clean verify | tee evidence/managed-only.log
test ! -f app/target/classes/build-marker.txt
jar tf app/target/app-1.0.0.jar | grep 'build-marker.txt' && echo UNEXPECTED || echo 'marker absent as predicted'
grep 'maven-antrun-plugin' evidence/managed-only.log || true
Expected observation: tests and JAR packaging
succeed, but there is no write-build-marker execution
in the build log and no marker in the JAR. This is the practical
difference between managed configuration and active execution.
8. Activate AntRun in the child—without copying its version/execution
Replace the child POM with the active form. The only new model fact
is the child's reference to maven-antrun-plugin.
Version, phase, goal, and configuration still come from the parent
policy.
<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>dev.academy</groupId>
<artifactId>plugin-lab-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>app</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><artifactId>maven-resources-plugin</artifactId></plugin>
<plugin><artifactId>maven-compiler-plugin</artifactId></plugin>
<plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
<plugin><artifactId>maven-jar-plugin</artifactId></plugin>
<plugin><artifactId>maven-antrun-plugin</artifactId></plugin>
</plugins>
</build>
</project>
set -o pipefail
./mvnw -Dmaven.repo.local="$REPO" clean verify | tee evidence/active.log
./mvnw -Dmaven.repo.local="$REPO" org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -pl app -Doutput=evidence/effective-active.xml
grep -n -A24 -B4 'write-build-marker' evidence/effective-active.xml
jar tf app/target/app-1.0.0.jar | grep 'build-marker.txt'
unzip -p app/target/app-1.0.0.jar build-marker.txt
unzip -p app/target/app-1.0.0.jar META-INF/MANIFEST.MF | grep 'Build-Policy'
Expected evidence: the log identifies
maven-antrun-plugin:3.2.0:run with execution id
write-build-marker; the effective POM shows the
inherited execution; the JAR contains build-marker.txt;
and the managed JAR configuration contributes
Build-Policy: academy-plugin-lab to the manifest.
9. What changed, exactly?
| Layer | Before | After |
|---|---|---|
| Child POM | No AntRun plugin reference. |
AntRun plugin referenced under build/plugins.
|
| Parent policy | AntRun version/execution already managed. | Unchanged. |
| Effective child build model | Managed definition available but custom execution not activated. | Managed definition injected into active AntRun plugin. |
| Execution graph | No write-build-marker. |
Named AntRun execution at prepare-package.
|
| Filesystem | No generated marker. | Marker appears in target/classes. |
| Artifact | No marker entry. | Marker packaged by later JAR goal. |
10. Lifecycle execution versus direct goal invocation
The build above proves lifecycle binding. A direct command such as a
fully qualified ...:antrun:run is a different
invocation path: it requests that goal immediately. Do not use
direct-goal success as proof that a lifecycle execution is bound
correctly. Conversely, a lifecycle build can invoke a plugin you
never type on the command line because packaging/default/POM
bindings select it.
11. Challenge — choose the control, not a copied command
A teammate wants build-marker.txt inside the JAR but
proposes moving the execution to verify because “verify
is more complete.” Which control should you change, and why?
Target reasoning: the marker must exist before the
JAR is assembled at package. Keep the execution at
prepare-package (or another semantically justified
earlier phase). Running later cannot retroactively modify an
already-created artifact unless another goal rebuilds it—which would
be a different execution design.
12. Verification and cleanup
Before cleanup, retain only evidence you intentionally want:
effective POM snippets, plugin versions, execution log lines, and
artifact checksums. Then delete the disposable lab directory or its
isolated .lab-m2/target/evidence
state. Do not delete normal ~/.m2 contents.
sha256sum app/target/app-1.0.0.jar > evidence/jar.sha256
./mvnw --version > evidence/maven-version.txt
grep 'maven-antrun-plugin:3.2.0:run' evidence/active.log > evidence/plugin-execution-evidence.txt
Knowledge check
Why did the AntRun execution not run in the first build?
It was only managed in the parent and had no active plugin
reference in the child. pluginManagement supplied
policy but did not activate this custom plugin.
Which two pieces of evidence independently prove the execution ran?
The Maven log identifies the plugin/goal/execution id, and the generated marker appears in the expected filesystem/JAR location.
Why does the marker belong at
prepare-package rather than
verify?
The JAR is created during package; content intended
for that JAR must exist before packaging.
What does help:effective-pom prove?
The merged model Maven sees for the project, including inherited/managed plugin configuration. It does not by itself prove execution.
Summary
The lab separated four concepts that are often collapsed: central
plugin policy, active plugin declaration, lifecycle execution, and
produced artifact evidence. Parent pluginManagement can
keep versions/configuration reviewable. Child activation determines
whether a custom plugin participates. Execution ids and phases
explain when it runs. The effective POM plus logs/artifact contents
lets you verify causality.
Official references and version notes
Version-sensitive statements in this lesson were checked against Apache Maven primary documentation on 2026-08-23. The required path uses Maven 3.9.16 through Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the project release target, Maven Compiler Plugin 3.15.0, Surefire 3.5.4, Maven JAR Plugin 3.5.1, Maven Resources Plugin 3.5.0, Maven Help Plugin 3.5.2, and Maven AntRun Plugin 3.2.0. Maven 4 preview-only behavior is not required in this chapter.
The AntRun execution is used only as visible, harmless teaching work. Its own documentation recommends avoiding POM pollution and using purpose-built mechanisms where appropriate.
- Maven — Introduction to Plugins
- Maven — Guide to Configuring Plugins
- Maven — POM Reference / Plugin Management
- Maven 3.9.16 — Default Lifecycle Plugin Bindings
- Maven — Plugin Prefix Resolution
- Maven — Available Plugins
- Maven Help Plugin 3.5.2
- Maven Compiler Plugin 3.15.0
- Maven JAR Plugin 3.5.1
- Maven Resources Plugin 3.5.0
- Maven AntRun Plugin 3.2.0
- Apache Maven Wrapper
- Maven 3.9.16 Release Notes
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.