Tutorial: How to Create a Maven Plugin
Learn more about how to create a Maven plugin.
Join the DZone community and get the full member experience.
Join For FreeApache Maven is a widely-adopted build automation tool in the Java space, popular for its plugin options. The tool simplifies your build process by making it easy to find an existing plugin that satisfies almost all your product’s needs, such as verifying binary compatibility between versions and confirming your source files have license headers.
In this post, we are going to create a Maven Plugin that will run ‘git rev-parse’. You’ll need to have Java 8 and Apache Maven installed in order to get started; you can use SDKMAN for that.
You may also like: 10 Effective Tips on Using Maven
If you’d rather watch a video, I created a screencast of this blog post.
Create a New Maven Project
It shouldn’t be a surprise that I’m going to use Maven to build a new Maven plugin. You can use your favorite IDE to create a new project, but to keep things simple, I’m just going to create a new pom.xml
file manually (in a new directory named example-maven-plugin
:
<?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>com.okta.example</groupId>
<artifactId>example-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>Example Maven Plugin</name>
<description>An Example Maven Plugin</description>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
This is as simple as it gets; I’ve defined the Maven GAV (Group ID, Artifact ID, Version), a name, and most importantly, I’ve set the packaging
to maven-plugin
. While the group ID could be just about anything, it is strongly recommended to be in reverse domain name notation, similar to Java packages.
Add Maven Dependencies
Next up, I need to define a few dependencies on maven-core
, maven-plugin-api
, and maven-plugin-annotations
. These are all scoped as provided
, which means when the plugin runs, the actual version used will depend on the version of Apache Maven you have installed.
<dependencies>
<dependency>
<!-- plugin interfaces and base classes -->
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- needed when injecting the Maven Project into a plugin -->
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>3.6.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<!-- annotations used to describe the plugin meta-data -->
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
Plugins Build Plugins
Plugins are what actually give Maven its power, at its core Maven is just a plugin framework, so naturally, I will use a Maven plugin to build a Maven plugin with the Maven Plugin Plugin. Turtles all the way down!
The maven-plugin-plugin
is actually defined automatically because I used the packaging type of maven-plugin
above; to use a newer version, I can update the plugin in the pluginManagment
section:
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</project>
I’ve also included the Maven Site Plugin; this is optional — more on that later in the post.
That is it! If you want to copy and paste the whole file all at once, you can grab it from GitHub.
Write the Maven Plugin Code
On to the fun part: writing the code! A Maven plugin is actually a collection of one or more “goals.” Each goal is defined by a Java class referred to as a “Mojo” (Maven plain Old Java Object).
Create a new class: src/main/java/com/okta/example/maven/GitVersionMojo.java
package com.okta.example.maven;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
/**
* An example Maven Mojo that resolves the current project's git revision and adds
* that a new {@code exampleVersion} property to the current Maven project.
*/
@Mojo(name = "version", defaultPhase = LifecyclePhase.INITIALIZE)
public class GitVersionMojo extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
// The logic of our plugin will go here
}
}
There isn’t much to it. Now, I have a new Maven Plugin, which has a single goal named version
. This goal will execute when the project is initialized. There are a few lifecycles to pick from; for this example, I’m using “initialize” because I want my plugin to run before other plugins. If you were creating a plugin to create new files, you would likely want to use the “generate-resources” phase. Take a look at the lifecycle reference documentation for descriptions of other phases.
At this point, I could build the project with mvn install
and then execute the plugin using:
# mvn ${groupId}:${artifactId}:${goal}
mvn com.okta.example:example-maven-plugin:version
However, since the execute
method is empty, it won’t actually do anything yet.
Adding Maven Parameters
To make this plugin actually do something, I’m going to add a couple of parameters. Maven parameters are defined as fields in the MOJO class:
/**
* The git command used to retrieve the current commit hash.
*/
@Parameter(property = "git.command", defaultValue = "git rev-parse --short HEAD")
private String command;
@Parameter(property = "project", readonly = true)
private MavenProject project;
It’s worth noting the Javadoc is important for Maven Plugins, as it will be used when we generate the plugin-specific documentation. Since we are all great developers, we never forget to add the doc, right?
The Parameter
annotation tells Maven to inject a value into the field. This is similar to Spring’s Value
annotation. For the command
field, I’ve set property
value to be git.command
. This allows the user to change the value on the command line with the standard -D
notation:
mvn com.okta.example:example-maven-plugin:version \
-Dgit.command="git rev-parse --short=4 HEAD"
It’s also common to inject the MavenProject
in order to read or modify something in the project directly. For example, the MavenProject
gives you access to the dependencies and anything defined in a pom.xml
. In my case, I’m going to add an additional property that can be used later in the build.
Execute a Command in Java
Now we have the command
parameter, we need to execute it! Define a new getVersion
method to handle this logic:
public String getVersion(String command) throws MojoExecutionException {
try {
StringBuilder builder = new StringBuilder();
Process process = Runtime.getRuntime().exec(command);
Executors.newSingleThreadExecutor().submit(() ->
new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines().forEach(builder::append)
);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new MojoExecutionException("Execution of command '" + command
+ "' failed with exit code: " + exitCode);
}
// return the output
return builder.toString();
} catch (IOException | InterruptedException e) {
throw new MojoExecutionException("Execution of command '" + command
+ "' failed", e);
}
}
This uses Java’s built-in Runtime.exec()
and captures the output text. Any exceptions are rethrown as a MojoExecutionException
(which will cause a build failure.)
Next update the execute()
method:
public void execute() throws MojoExecutionException, MojoFailureException {
// call the getVersion method
String version = getVersion(command);
// define a new property in the Maven Project
project.getProperties().put("exampleVersion", version);
// Maven Plugins have built in logging too
getLog().info("Git hash: " + version);
}
That is it. Now, we just need to use the plugin!
Usage of a Maven Plugin
Up until now, I’ve been executing the plugin directly using the command:
mvn com.okta.example:example-maven-plugin:version
Usually, plugins are added to a pom.xml
so they are run automatically as part of a build. To demonstrate this, I’ll create a new Maven project with the following pom.xml
(in a different directory):
<?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>com.okta.example</groupId>
<artifactId>example-usage</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>com.okta.example</groupId>
<artifactId>example-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration>
<!-- optional, the command parameter can be changed here too -->
<command>git rev-parse --short=4 HEAD</command>
</configuration>
<executions>
<execution>
<goals>
<goal>version</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.ekryd.echo-maven-plugin</groupId>
<artifactId>echo-maven-plugin</artifactId>
<version>1.2.0</version>
<inherited>false</inherited>
<executions>
<execution>
<id>end</id>
<goals>
<goal>echo</goal>
</goals>
<phase>process-resources</phase>
<configuration>
<message>${line.separator}${line.separator}
The project version is ${project.version}-${exampleVersion}
${line.separator}
</message>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Running mvn package
on this project result gives the output:
The [INFO] Git hash: 1ab3
line shows my plugin as it executes, and the new exampleVersion
property defined by the plugin is used by the echo-maven-plugin.
NOTE: Once you have added a plugin to a
pom.xml
, you can use the shorthand notation to execute the plugin:mvn <prefix>:<goal>
, commonly the “prefix” is the artifact ID minus the “-maven-plugin”. For example,mvn example:version
.
Dependency Injection in Maven Plugins
Our plugin is great and all, but all of the code is crammed into one file. I like to break up my code into easily testable chunks. Enter Sisu, the container Maven is built on. Sisu is an IoC container built on top of Guice, an alternative to Spring.
What this all really means is that I can use the standard JSR-330 (@Inject
) annotations to break up my code and not worry about the details of the IoC container!
Create a new interface in src/main/java/com/okta/example/maven/VersionProvider.java
:
package com.okta.example.maven;
import org.apache.maven.plugin.MojoExecutionException;
public interface VersionProvider {
String getVersion(String command) throws MojoExecutionException;
}
And move the Runtime.exec
logic out of GitVersionMojo
into a new class src/main/java/com/okta/example/maven/RuntimeExecVersionProvider.java
:
package com.okta.example.maven;
import org.apache.maven.plugin.MojoExecutionException;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.Executors;
@Named
@Singleton
public class RuntimeExecVersionProvider implements VersionProvider {
@Override
public String getVersion(String command) throws MojoExecutionException {
try {
StringBuilder builder = new StringBuilder();
Process process = Runtime.getRuntime().exec(command);
Executors.newSingleThreadExecutor().submit(() ->
new BufferedReader(new InputStreamReader(process.getInputStream())).lines().forEach(builder::append)
);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new MojoExecutionException("Execution of command '" + command + "' failed with exit code: " + exitCode);
}
// return the output
return builder.toString();
} catch (IOException | InterruptedException e) {
throw new MojoExecutionException("Execution of command '" + command + "' failed", e);
}
}
}
I’ve added the standard Java @Named
and @Singleton
annotations to mark the class as a singleton that is managed by the IoC container. This is the equivalent of using Spring’s @Component
.
Now just update the GitVersionMojo
to inject the VersionProvider
:
@Inject
private VersionProvider versionProvider;
public void execute() throws MojoExecutionException, MojoFailureException {
String version = versionProvider.getVersion(command);
project.getProperties().put("exampleVersion", version);
getLog().info("Git hash: " + version);
}
That is it! You could build and run the plugin as before, and get the same results.
One More Thing, Documentation!
One of my favorite things about Maven is that plugins have consistent documentation structures. To generate the documentation, add a new reporting
section to the pom.xml
:
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<reportSets>
<reportSet>
<reports>
<report>report</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
You should also add more metadata to your project, but this is optional. For example, I’ll add the organization and prerequisites, as these are included in the generated site:
<organization>
<name>Example, Inc</name>
<url>https://google.com/search?q=example.com</url>
</organization>
<prerequisites>
<maven>3.5.0</maven>
</prerequisites>
Now, just run mvn site
to generate the documentation! Open the target/site/plugin-info.html
in your browser. See why all of that Javadoc was so important?
Learn More
As always, you can find the full source code for this tutorial on GitHub. To learn more about building plugins, the Apache Maven project has great documentation. Check out these other tutorials as well:
- Get started with Okta in seconds with the Okta Maven plugin
- A Quick Guide to Spring Boot Login Options
- Make Java Tests Groovy With Hamcrest
- License Maven Plugin
- japicmp - a Maven plugin for binary, source, and semver validation
If you liked this tutorial, follow us on Twitter @oktadev. We also publish weekly video tutorials on our YouTube channel.
How to Build a Maven Plugin was originally published on the Okta Developer Blog on September 23, 2019.
Published at DZone with permission of Brian Demers, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments