skip to content

What does it take to write your own Maven plugin, and what are the key annotations and packaging involved?

level: seniorimportance: nice to knowfreq 25%

answer

  1. packaging maven-plugin
  2. extend AbstractMojo, execute()
  3. @Mojo(name, defaultPhase)
  4. @Parameter injects config
  5. maven-plugin-plugin generates plugin.xml
  6. MojoExecutionException vs MojoFailureException

basics

~10 s

Create a project with <packaging>maven-plugin</packaging>, depend on maven-plugin-api, and write a class extending AbstractMojo. Annotate it with @Mojo(name="...") and @Parameter fields, implement execute(), and the maven-plugin-plugin generates the descriptor.

solid answer

~40 s

A Maven plugin is a normal Maven project with <packaging>maven-plugin</packaging>. You add maven-plugin-api (the AbstractMojo, MojoExecutionException, logging) and maven-plugin-annotations as dependencies, and the maven-plugin-plugin to the build to generate the plugin descriptor (META-INF/maven/plugin.xml). Each goal is a class extending AbstractMojo, annotated @Mojo(name = "greet", defaultPhase = LifecyclePhase.COMPILE), with @Parameter-annotated fields injected from the pom <configuration> (with property aliases and defaultValue). You override execute(), which throws MojoExecutionException (build error) or MojoFailureException (expected failure). You access the build via injected components like @Parameter(defaultValue = "${project}", readonly = true) MavenProject project. Goals can declare @Mojo(requiresDependencyResolution = ...) to get the classpath. After mvn install, others use it via coordinates and the goalPrefix you set on the maven-plugin-plugin. The descriptor is what makes goals discoverable.

code

java · 11 lines
java
@Mojo(name = "greet", defaultPhase = LifecyclePhase.COMPILE)
public class GreetMojo extends AbstractMojo {
    @Parameter(property = "greet.name", defaultValue = "world")
    private String name;

    @Override
    public void execute() throws MojoExecutionException {
        if (name.isBlank()) throw new MojoExecutionException("name required");
        getLog().info("Hello, " + name);
    }
}

go deeper

for a junior

Aware that custom plugins exist and contain mojos, without needing to write one.

for a middle

Can sketch a mojo: AbstractMojo, execute(), @Parameter for config.

for a senior

Knows packaging, descriptor generation, exception semantics, component injection, and dependency-resolution requirements.

for a principal

Designs reusable internal plugins with stable parameter APIs, thread-safety, integration tests, and a versioning/release strategy.

## Project setup A plugin is built like any artifact, but with a special packaging: ```xml <packaging>maven-plugin</packaging> <dependencies> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>3.9.6</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.maven.plugin-tools</groupId> <artifactId>maven-plugin-annotations</artifactId> <version>3.13.0</version> <scope>provided</scope> </dependency> </dependencies> ``` The `maven-plugin` packaging activates a lifecycle that runs the **maven-plugin-plugin** to scan your annotated classes and generate `META-INF/maven/plugin.xml` — the *plugin descriptor*. The descriptor lists each goal, its parameters, default phase, and the implementing class. Without it, Maven can't discover your goals. ## A goal = a Mojo ```java @Mojo(name = "greet", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.COMPILE) public class GreetMojo extends AbstractMojo { @Parameter(property = "greet.name", defaultValue = "world") private String name; @Parameter(defaultValue = "${project}", readonly = true, required = true) private MavenProject project; @Override public void execute() throws MojoExecutionException { getLog().info("Hello, " + name + " from " + project.getArtifactId()); } } ``` Key pieces: - **`extends AbstractMojo`** gives you `getLog()` and forces `execute()`. - **`@Mojo`** names the goal and sets defaults: `defaultPhase`, `requiresDependencyResolution` (so the project classpath is resolved), `threadSafe`, `aggregator`, etc. - **`@Parameter`** binds a field to pom `<configuration>`. `property` exposes a `-D` / property alias, `defaultValue` supports `${...}` expressions, `required`/`readonly` add constraints. A user setting `<name>Maven</name>` in `<configuration>` injects into this field. - **`execute()`** does the work. Throw `MojoExecutionException` for unexpected internal errors (printed with stack trace) and `MojoFailureException` for expected build-rule failures (e.g. a check that failed) — the distinction affects how Maven reports it. ## Accessing Maven internals Inject components via `@Parameter(defaultValue = "${...}", readonly = true)` (e.g. `${project}`, `${session}`, `${settings}`) or via `@Component` for plexus/Sisu components like a `RepositorySystem`. `requiresDependencyResolution` controls which scope of the project's classpath is resolved and available. ## Publishing and using it `mvn install`/`deploy` puts the plugin in a repository. Set its prefix via the maven-plugin-plugin's `<goalPrefix>` (or rely on the `*-maven-plugin` naming auto-derivation). Consumers then run `mvn yourprefix:greet` or bind it in `<executions>`. ## Testing Use `maven-plugin-testing-harness` for unit tests of mojos, and the **maven-invoker-plugin** for integration tests that run real builds against sample projects under `src/it`.

  • What generates the plugin descriptor and why is it needed?
    The maven-plugin-plugin scans @Mojo/@Parameter annotations and emits META-INF/maven/plugin.xml; Maven reads it to discover goals and their parameters.
  • Difference between MojoExecutionException and MojoFailureException?
    ExecutionException = unexpected internal error (stack trace); FailureException = an expected, controlled build failure like a violated rule.
  • How does a mojo get the current MavenProject?
    Inject it: @Parameter(defaultValue = "${project}", readonly = true) MavenProject project.

saying these in an interview costs you the question

  • Forgetting <packaging>maven-plugin</packaging> or the maven-plugin-plugin — then no descriptor is generated and goals aren't discoverable.
  • Confusing MojoExecutionException and MojoFailureException semantics.
  • Thinking you implement an interface named 'Mojo' directly rather than extending AbstractMojo with @Mojo annotation (modern approach).

context