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.txtRemember
- 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)