Chapter 07Lesson 02~170 minutes

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.

Effective POMNamed ExecutionsAntRunParent POMPlugin Help

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 pluginManagement is inactive until referenced by the child.
  • Activate a named execution bound to prepare-package and verify the generated marker enters the packaged JAR.
Current baseline — verified 2026-08-23. Required labs use Maven 3.9.16 via Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the compiler release target, Compiler Plugin 3.15.0, Surefire 3.5.4, JAR Plugin 3.5.1, Resources Plugin 3.5.0, Help Plugin 3.5.2, and AntRun Plugin 3.2.0. Every lab uses a disposable project directory and -Dmaven.repo.local=<lab>/.lab-m2/repository. Normal user caches/settings are not deleted or rewritten.
Cross-platform note: POSIX examples use ./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?

Which two pieces of evidence independently prove the execution ran?

Why does the marker belong at prepare-package rather than verify?

What does help:effective-pom prove?

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.

Next lesson

Choose the policy shape deliberately

Lesson 3 compares default bindings, explicit executions, pluginManagement, inheritance boundaries, and plugin version strategies for maintainable production builds.

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.