Writing Maven plugins and extensions

Maven · Expert · 14 min

Custom plugins encode organization-specific build steps; understanding Mojos also demystifies every plugin you configure.

A Maven plugin is a jar with packaging maven-plugin containing Mojos (Maven plain Old Java Objects): classes annotated with @Mojo(name = "goal", defaultPhase = LifecyclePhase.X) that extend AbstractMojo and implement execute().

Parameters are fields annotated with @Parameter(property = "greeting.name", defaultValue = "World"). Users configure them in <configuration> or via -Dgreeting.name=Ada. Inject build context with @Parameter(defaultValue = "${project}", readonly = true) MavenProject project, or ${session}, ${mojoExecution}. Components such as the repository system are injected with @Inject (or the older @Component).

Mojo flags: requiresDependencyResolution = ResolutionScope.COMPILE to get the resolved classpath, threadSafe = true for -T builds, aggregator = true to run once at the root of a reactor.

The maven-plugin-plugin generates the plugin descriptor (META-INF/maven/plugin.xml) from the annotations at build time. Users apply the plugin like any other, by coordinates; a <pluginGroup> in settings lets them use the short prefix:goal form.

Testing: maven-plugin-testing-harness for unit tests, and maven-invoker-plugin to run real sample projects under src/it as integration tests. Log with getLog(), fail with MojoFailureException (build problem) or MojoExecutionException (unexpected error).

Core extensions (.mvn/extensions.xml) hook into Maven itself rather than the lifecycle: CI-friendly versioning (maven-git-versioning-extension), build caching (maven-build-cache-extension), or Develocity/Gradle Enterprise build scans. Maven 4 keeps the plugin API largely compatible (org.apache.maven.api offers a new one), so plugins written this way keep working.

Example

// src/main/java/com/acme/GreetMojo.java in a project with <packaging>maven-plugin</packaging>
package com.acme;
import org.apache.maven.plugin.*;
import org.apache.maven.plugins.annotations.*;
import org.apache.maven.project.MavenProject;
import java.nio.file.*;
@Mojo(name = "greet", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true)
public class GreetMojo extends AbstractMojo {
    @Parameter(property = "greet.name", defaultValue = "World")
    private String name;
    @Parameter(defaultValue = "${project.build.directory}/greeting.txt", required = true)
    private Path output;
    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;
    @Override
    public void execute() throws MojoExecutionException {
        try {
            Files.createDirectories(output.getParent());
            Files.writeString(output, "Hello, " + name + " from " + project.getArtifactId());
            getLog().info("Wrote " + output);
        } catch (java.io.IOException e) {
            throw new MojoExecutionException("could not write greeting", e);
        }
    }
}
// consumer: <plugin><groupId>com.acme</groupId><artifactId>greet-maven-plugin</artifactId>
//   <version>1.0.0</version><executions><execution><goals><goal>greet</goal></goals></execution></executions></plugin>
// $ mvn generate-resources -Dgreet.name=Ada   -> [INFO] Wrote .../target/greeting.txt

Remember

  • A Mojo is an annotated class with execute(); @Parameter fields are its configuration
  • defaultPhase binds the goal; ${project} and @Inject give build context
  • Test with the plugin testing harness and invoker integration tests
  • Core extensions hook into Maven itself (caching, versioning)

Check yourself

  1. How does Maven know which phase a goal runs in when the user doesn't specify <phase>?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: maven.apache.org/plugin-developers/

Builds on

Unlocks

Nothing builds on this yet.