How to write your own Maven plugins

Creating custom plugins for the Apache Maven build tool is easier than you might think.

August 3, 2020

Download a PDF of this article

Many Java developer tools can be extended with plugins, which are small pieces of functionality that deeply integrate with the tool to provide useful features. The Eclipse IDE, the Jenkins automation server, and the Maven build tool are three examples. Coders often use prewritten plugins—but it’s easy to create them as well. Since many tools used by Java developers are written in Java, so are the plugins.

As an extra bonus, writing plugins can deepen your understanding of the system those plugins plug in to. Let’s see how that works by writing plugins for (almost) everyone’s favorite build management tool: Apache Maven.

The basics

The core of most any Maven plugin is a single Java class called a Maven plain old Java object (Mojo). That term is a pun on the well-known term plain old Java object (Pojo). It’s an open question whether a Mojo would actually qualify as a Pojo as well, since a Pojo has the requirement to implement an interface.

A Mojo in Maven is a managed object. Specifically, it’s managed by dependency injection (DI) for Java (JSR 330) and its compatible modular container called Eclipse Sisu, which started life in March 2012.

To dig deeper, Sisu itself builds on the well-known lightweight implementation of JSR 330 called Guice, a DI container by Bob Lee, who was among the initial DI pioneers in Java. Sisu adds classpath scanning, autobinding, and autowiring to Guice, making it a little bit more similar to context and dependency injection (CDI), where those things also happen automatically. Contrast Sisu’s CDI to plain Guice, where binding (that is, matching the preferred implementation for injection to an interface) must be done programmatically.

Looking at it from a metaperspective, Sisu, which makes plugins for Maven possible, is itself essentially a plugin for Guice.

The observant reader might realize that Sisu is from 2012, but Maven goes back to July 2004. Well, initially Maven used an inversion-of-control (IoC) container called Plexus. While Plexus was essentially a standalone IoC container in its own right, not necessarily tied to Maven, it was created by the same people who were also behind Maven. In all its years, Plexus found little adoption outside Maven. Therefore, it practically became Maven’s private IoC container.

Having to maintain such a private container when comparable alternatives exist in open source rarely makes sense, so in 2010 with Maven 3 emerging, the team decided to switch to Guice with a few extensions of their own. Maven 3.0 (October 2010) initially used Guice only under the covers with a compatibility layer in place such that all existing Plexus-based plugins continued to run. It wasn’t until Maven 3.1 in July 2013 that Guice and the Maven-contributed extensions (by then called Sisu and moved to Eclipse) were finally opened for public consumption by plugins.

Because of the long history of Plexus and the seamless compatibility layer put in place, Plexus is still often encountered in Maven plugins.

Now that you’ve heard all that history, let’s build a Maven plugin.

Building a “hello world” plugin

Let’s start with the following “hello world” plugin:


package org.omnifaces.mojo.group;

import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;

@Mojo(name = "hello")
public class HelloMojo implements org.apache.maven.plugin.Mojo {

    private Log log;

    @Override
    public void execute() {
        log.info("Hello, world");
    }

    @Override
    public void setLog(Log log) {
        this.log = log;
    }

    @Override
    public Log getLog() {
        return log;
    }
}

Here’s the fairly minimal pom.xml to build it:


<?xml version="1.0" encoding="UTF-8"?>

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.omnifaces.example</groupId>
    <artifactId>hello-maven-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>maven-plugin</packaging>
    
    <prerequisites>
        <maven>3.5</maven>
    </prerequisites>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.plugin.skipErrorNoDescriptorsFound>true</maven.plugin.skipErrorNoDescriptorsFound>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.6.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.6.3</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

The major difference between this and a JAR project is that the packaging is of type maven-plugin and there must be a prerequisites element to set the minimal Maven version. There’s also a required property, maven.plugin.skipErrorNoDescriptorsFound, that always needs to be set to false. I use two dependencies here: maven-plugin-annotations delivers the @Mojo annotation, while maven-plugin-api provides the Mojo interface (and several related types such as exceptions, an abstract implementation, and the Log type shown), as well as Sisu, which in turn brings in the JSR 330 (and JSR 250) annotations. Oh, and some Plexus stuff is thrown in as well for good measure.

What the maven-plugin packaging primarily brings to the table is that the maven-plugin-plugin (that repetition is intentional; it’s not a typo) is added to the lifecycle, which in turn generates a META-INF/maven/plugin.xml file. This is somewhat comparable to Open Services Gateway initiative (OSGi) projects, where “bundle” packaging adds a bundle plugin, which generates a special META-INF/MANIFEST.MF.

The generated plugin.xml file looks like this:


<?xml version="1.0" encoding="UTF-8"?>

<!-- Generated by maven-plugin-tools 3.2 -->

<plugin>
  <name>hello-maven-plugin</name>
  <description></description>
  <groupId>org.omnifaces.example</groupId>
  <artifactId>hello-maven-plugin</artifactId>
  <version>1.0-SNAPSHOT</version>
  <goalPrefix>hello</goalPrefix>
  <isolatedRealm>false</isolatedRealm>
  <inheritedByDefault>true</inheritedByDefault>
  <mojos>
    <mojo>
      <goal>hello</goal>
      <requiresDirectInvocation>false</requiresDirectInvocation>
      <requiresProject>true</requiresProject>
      <requiresReports>false</requiresReports>
      <aggregator>false</aggregator>
      <requiresOnline>false</requiresOnline>
      <inheritedByDefault>true</inheritedByDefault>
      <implementation>org.omnifaces.mojo.group.HelloMojo</implementation>
      <language>java</language>
      <instantiationStrategy>per-lookup</instantiationStrategy>
      <executionStrategy>once-per-session</executionStrategy>
      <threadSafe>false</threadSafe>
      <parameters/>
    </mojo>
  </mojos>
  <dependencies/>
</plugin>

You can see that the <mojo> element largely corresponds to the @Mojo annotation, with many of its default attributes explicitly defined.

Execute the custom plugin by typing the following on the command line:


mvn org.omnifaces.example:hello-maven-plugin:hello

Then you should see something like this:


[INFO] --------------< org.omnifaces.example:hello-maven-plugin >--------------
[INFO] Building hello-maven-plugin 1.0-SNAPSHOT
[INFO] ----------------------------[ maven-plugin ]----------------------------
[INFO] 
[INFO] --- hello-maven-plugin:1.0-SNAPSHOT:hello (default-cli) @ hello-maven-plugin ---
[INFO] Hello, world

Because this is a lot of typing on the command line, Maven has an option to shorten this that uses so-called plugin groups and goal prefixes. The way this works is that for a specific number of group IDs, Maven allows you to omit the group ID, and instead of using the full artifact ID you can use the shorter goal prefix. The plugin groups that Maven checks can be specified in a settings.xml file (such as ~/.m2/settings.xml):


<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <pluginGroups>
    <pluginGroup>org.omnifaces.example</pluginGroup>
  </pluginGroups>
</settings>

With that in place, you can now invoke the plugin as follows:


mvn hello:hello

Debugging Maven plugins

If you are building custom plugins, you need to be able to debug them. It’s perhaps not always immediately clear how to do this. After all, developers don’t simply start a Mojo via the IDE; they have to connect to the actual mvn command in some way. Luckily Maven has a default solution for this, namely the mvnDebug command. This command suspends execution right after invocation so you can attach a remote debugger to a port. By default, that port is 8000, as shown the following output:


mvnDebug hello:hello

Preparing to execute Maven in debug mode
Listening for transport dt_socket at address: 8000

In an IDE, you can connect to that port and then you’ll see what’s shown in Figure 1. (I’m using Eclipse here.)

What you see after connecting to the port

Figure 1. What you see after connecting to the port

Debugging also offers an opportunity to discover and learn more about how the code is being invoked by the system. In Eclipse (and pretty much every other IDE), you can easily go back a step in the call stack. The plugin does need to have the correct source attachment. In this case, DefaultBuildPluginManager is in the org.apache.maven:maven-core dependency, and the Maven version being used at the command line when writing this happens to be 3.6.3.

The easiest way to give Eclipse access to the source is to add the provided dependency, org.apache.maven:maven-core dependency:3.6.3, to the project’s pom.xml. After doing that, peek at the code that called the Mojo (see Figure 2):

Code that calls the Mojo

Figure 2. Code that calls the Mojo

Injecting a dependency

Let’s expand the “hello world” example by introducing a dependency that will be injected into the Mojo that was previously shown. In this case, it’s a component that asks the user for input. By the way, for plugins that participate in the build of a project, it’s usually not a good idea to prompt the user, because builds almost always need to run unattended and often they need to be reproducible. There are, however, also Maven plugins that primarily interact with the user where automation is not a concern.

The component for this example can be obtained by adding a dependency to org.codehaus.plexus:plexus-interactivity-api:1.0 to the POM. The expanded code looks as follows:


@Mojo(name = "hello")
public class HelloMojo implements org.apache.maven.plugin.Mojo {

    @Inject
    private Prompter prompter;

    private Log log;

    @Override
    public void execute() {
        try {
            String name = prompter.prompt("What's your name?");
            log.info("Hello, " + name);
        } catch (PrompterException e) {
            new MojoExecutionException("Something went wrong", e);
        }
    }

    @Override
    public void setLog(Log log) {
        this.log = log;
    }

    @Override
    public Log getLog() {
        return log;
    }
}

Running this via the command mvn hello:hello will indeed prompt the user to enter something:


[INFO] Building hello-maven-plugin 1.0-SNAPSHOT
[INFO] ----------------------------[ maven-plugin ]----------------------------
[INFO] 
[INFO] --- hello-maven-plugin:1.0-SNAPSHOT:hello (default-cli) @ hello-maven-plugin ---
What's your name?: Arjan
[INFO] Hello, Arjan
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Note that as the name of the dependency already suggested, Prompter here is a Plexus component. Even though Sisu was made publicly available in 2013, seven years later an overwhelming number of components for Maven are Plexus components.

Manipulating POMs

A specific class of plugins for Maven consists of interactive plugins that let the user manipulate the project’s POM files. Two well-known examples are versions, which is used when you want to manage the versions of artifacts in a POM, and tidy, which cleans up a project’s pom.xml file.

The advantage of using a Maven plugin for these tasks, instead of using just any standalone tool, is that the POM files are already parsed by Maven and are “in context,” meaning they are processed according to how Maven actually sees them with respect to parent-child relationships, resolving dependencies, parameters, and so on.

Let’s now create a plugin to change the group ID of a project. While the Maven APIs have essentially everything in them to navigate the project structure, writing out changes to the POM files is not covered much. Therefore, let’s use the versions plugin as a dependency to reuse some of its code for this.

The base structure of the Mojo looks as follows:


@Mojo(name = "setGroup", requiresProject = true, requiresDirectInvocation = true, aggregator = true, threadSafe = true)
public class SetGroupMojo extends AbstractVersionsUpdaterMojo {

    @Parameter(property = "newGroupId")
    private String newGroupId;

    @Parameter(property = "groupId", defaultValue = "${project.groupId}")
    private String groupId;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (project.getOriginalModel().getGroupId() == null) {
            throw new MojoExecutionException("Project GroupId is inherited from parent.");
        }

        try {
            for (File pomFile : collectPomFiles(getMavenProject(), getReactorModels(project, getLog()))) {
                process(pomFile);
            }
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }
}

There are a few new things here: Since the plugin is operating on a project, it requires defining such a project via the @Mojo annotation. It also requires that the plugin is directly invoked (on the command line) and not attached to the lifecycle via a pom.xml file.

The @Parameter annotated fields allow for the injection of parameters, which can come from the configuration section of a plugin in a pom.xml file or from -D definition properties on the command line. Specifically, while Mojos are managed by the JSR 330 container that handles injection, the @Parameter annotation is not JSR 330–related. The Eclipse MicroProfile Config, for instance, which provides something rather similar, does use the JSR 330 @Inject and @Qualifier annotations. Another thing to note here is the common pattern in Mojos of injecting various things by means of expressions in the defaultValue attribute of a @Parameter annotated field. For example, settings are often injected like this:


@Parameter(defaultValue = "${settings}", readonly = true )
private Settings settings;

Here the group ID of the current project is injected, but it can be overridden by a -D parameter.

The SetGroupMojo Mojo shown above uses the reactor map (a map keyed by the relative module path) to collect all modules, and then with the help of the MavenProject class, these are converted to absolute paths:


  private Set<File> collectPomFiles(MavenProject project, Map<String, Model> reactor) {
        return
            reactor.entrySet().stream()
                   .filter(e -> getGroupId(e.getValue()) != null)
                   .filter(e -> getArtifactId(e.getValue()) != null)
                   .map(e -> getModuleFile(project, e.getKey()))
                   .collect(toSet());
  }

  private File getModuleFile(MavenProject project, String relativeModulePath) {
        File moduleDir = new File(project.getBasedir(), relativeModulePath);

        if (project.getBasedir().equals(moduleDir)) {
            return project.getFile();
        }

        if (moduleDir.isDirectory()) {
            return new File(moduleDir, "pom.xml");
        }

        return moduleDir;
    }

Subsequently, with a little magic provided by the base class, each POM file is updated with the new group ID:


@Override
    protected synchronized void update(ModifiedPomXMLEventReader pom) throws MojoExecutionException, MojoFailureException, XMLStreamException {
        try {
            Model model = getRawModel(pom);
            Parent parent = model.getParent();

            // Update parent
            if (parent != null && groupId.equals(parent.getGroupId())) {
                setProjectValue(pom, "/project/parent/groupId", newGroupId);
            }

            // Update project
            if (groupId.equals(getGroupId(model))) {
                setProjectValue(pom, "/project/groupId", newGroupId);
            }
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

This is a small example, of course, and many things a more robust group ID changer plugin would do are omitted for brevity. The code didn’t update any plugin section, for instance (which is useful if the project refers to its own modules). The plugin would probably need the ability to say something about subgroups as well, such as by renaming the foo.bar in foo.bar.kaz. I’ll leave those exercises for the reader.

Conclusion

It’s relatively simple to start writing custom Maven plugins. However, due to Maven’s rich history, it may be a little confusing at first what the various injection options are.

This article looked at two variants of a “hello world” plugin: one just displaying some text and the other asking for user input. The examples also looked at a plugin that manipulates the project’s POM files.

Naturally this only scratches the surface of what is possible with Maven plugins. Using custom Java code as well as taking advantage of Maven’s large API and available components, the sky’s the limit.

Arjan Tijms

Arjan Tijms was a JSF (JSR 372) and Security API (JSR 375) EG member and is currently project lead for a number of Jakarta projects including Jakarta- Security, Authentication, Authorization, Faces, and Expression Language. He is the co-creator of the popular OmniFaces library for JSF that was a 2015 Duke's Choice Award winner and is the author of two books: The Definitive Guide to JSF and Pro CDI 2 in Java EE 8. Arjan holds an MSc degree in computer science from the University of Leiden, The Netherlands. Follow Arjan on Twitter at @arjan_tijms.

Share this Page