11.4. Writing a Custom Plugin
When you write a custom plugin, you are going to be writing a series
of Mojos (goals). Every Mojo is a single Java class which contains a
series of annotations that tell Maven how to generate the Plugin
descriptor described in the previous section. Before you can start
writing Mojo classes, you will need to create Maven project with the
appropriate packaging and POM.
11.4.1. Creating a Plugin Project
To create a plugin project, you should use the Maven Archetype
plugin. The following command-line will create a plugin with a
groupId
of org.sonatype.mavenbook.plugins
and the artifactId
of
first-maven-plugin
:
$ mvn archetype:create \
-DgroupId=org.sonatype.mavenbook.plugins \
-DartifactId=first-maven-plugin \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-mojo
The Archetype plugin is going to create a directory named
my-first-plugin which contains the following POM.
A Plugin Project’s POM.
<?xml version="1.0" encoding="UTF-8"?><project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.sonatype.mavenbook.plugins</groupId>
<artifactId>first-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>first-maven-plugin Maven Mojo</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
The most important element in a plugin project’s POM is the packaging
element which has a value of maven-plugin
. This packaging element
customizes the Maven lifecycle to include the necessary goals to
create a plugin descriptor. The plugin lifecycle was introduced in
Section 4.2.3, “Maven Plugin”, it is similar to the Jar
lifecycle with three exceptions: plugin:descriptor
is bound to the
generate-resources
phase, plugin:addPluginArtifactMetadata
is
added to the package
phase, and plugin:updateRegistry
is added to
the install
phase.
The other important piece of a plugin project’s POM is the dependency
on the Maven Plugin API. This project depends on version 2.0 of the
maven-plugin-api
and it also adds in JUnit as a test-scoped
dependency.
11.4.2. A Simple Java Mojo
In this chapter, we’re going to introduce a Maven Mojo written in
Java. Each Mojo in your project is going to implement the
org.apache.maven.plugin.Mojo
interface, the Mojo
implementation
shown in the following example implements the Mojo interface by
extending the org.apache.maven.plugin.AbstractMojo
class. Before we
dive into the code for this Mojo, let’s take some time to explore the
methods on the Mojo interface. Mojo provides the following methods:
-
void setLog( org.apache.maven.monitor.logging.Log log )
-
Every
Mojo
implementation has to provide a way for the plugin to
communicate the progress of a particular goal. Did the goal
succeed? Or, was there a problem during goal execution? When Maven
loads and executes a Mojo, it is going to call the setLog()
method and supply the Mojo instance with a suitable logging
destination to be used in your custom plugin.
-
protected Log getLog()
-
Maven is going to call
setLog()
before your Mojo
is executed,
and your Mojo
can retrieve the logging object by calling
getLog()
. Instead of printing out status to Standard Output or
the console, your Mojo
is going to invoke methods on the Log
object.
-
void execute() throws org.apache.maven.plugin.MojoExecutionException
-
This method is called by Maven when it is time to execute your
goal.
The Mojo
interface is concerned with two things: logging the results
of goal execution and executing a goal. When you are writing a custom
plugin, you’ll be extending AbstractMojo
. AbstractMojo
takes care
of handling the setLog()
and getLog()
implementations and contains
an abstract execute()
method. When you extend AbstractMojo
, all
you need to do is implement the execute()
method. A Simple EchoMojo shows a trivial Mojo
implement which
simply prints out a message to the console.
A Simple EchoMojo.
package org.sonatype.mavenbook.plugins;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
/**
* Echos an object string to the output screen.
* @goal echo
* @requiresProject false
*/
public class EchoMojo extends AbstractMojo
{
/**
* Any Object to print out.
* @parameter expression="${echo.message}" default-value="Hello World..."
*/
private Object message;
public void execute()
throws MojoExecutionException, MojoFailureException
{
getLog().info( message.toString() );
}
}
If you create this Mojo in ${basedir} under src/main/java in
org/sonatype/mavenbook/mojo/EchoMojo.java in the project created in
the previous section and run mvn install
, you should be able to
invoke this goal directly from the command-line with:
$ mvn org.sonatype.mavenbook.plugins:first-maven-plugin:1.0-SNAPSHOT:echo
That large command-line is mvn
followed by the
groupId:artifactId:version:goal
. When you run this command-line you
should see output that contains the output of the echo goal with the
default message: "Hello Maven World…". If you want to customize the
message, you can pass the value of the message parameter with the
following command-line:
$ mvn org.sonatype.mavenbook.plugins:first-maven-plugin:1.0-SNAPSHOT:echo \
-Decho.message="The Eagle has Landed"
The previous command-line is going to execute the EchoMojo
and print
out the message "The Eagle has Landed".
11.4.3. Configuring a Plugin Prefix
Specifying the groupId
, artifactId
, version
, and goal
on the
command-line is cumbersome. To address this, Maven assigns a plugin a
prefix. Instead of typing:
$ mvn org.apache.maven.plugins:maven-jar-plugin:2.2:jar
You can use the plugin prefix jar
and turn that command-line into
mvn jar:jar
. How does Maven resolve something like jar:jar
to
org.apache.mven.plugins:maven-jar:2.3
? Maven looks at a file in the
Maven repository to obtain a list of plugins for a specific
groupId
. By default, Maven is configured to look for plugins in two
groups: org.apache.maven.plugins
and org.codehaus.mojo
. When you
specify a new plugin prefix like mvn hibernate3:hbm2ddl
, Maven is
going to scan the repository metadata for the appropriate plugin
prefix. First, Maven is going to scan the org.apache.maven.plugins
group for the plugin prefix hibernate3
. If it doesn’t find the
plugin prefix hibernate3
in the org.apache.maven.plugins
group it
will scan the metadata for the org.codehaus.mojo
group.
When Maven scans the metadata for a particular groupId
, it is
retrieving an XML file from the Maven repository which captures
metadata about the artifacts contained in a group. This XML file is
specific for each repository referenced, if you are not using a custom
Maven repository, you will be able to see the Maven metadata for the
org.apache.maven.plugins
group in your local Maven repository
(~/.m2/repository) under
org/apache/maven/plugins/maven-metadata-central.xml. Maven Metadata for the Maven Plugin Group
shows a snippet of the maven-metadata-central.xml file from the
org.apache.maven.plugin
group.
Maven Metadata for the Maven Plugin Group.
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<plugins>
<plugin>
<name>Maven Clean Plugin</name>
<prefix>clean</prefix>
<artifactId>maven-clean-plugin</artifactId>
</plugin>
<plugin>
<name>Maven Compiler Plugin</name>
<prefix>compiler</prefix>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<name>Maven Surefire Plugin</name>
<prefix>surefire</prefix>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
...
</plugins>
</metadata>
As you can see in Maven Metadata for the Maven Plugin Group, this
maven-metadata-central.xml file in your local repository is what
makes it possible for you to execute mvn surefire:test
. Maven scans
org.apache.maven.plugins
and org.codehaus.mojo
: plugins from
org.apache.maven.plugins
are considered core Maven plugins and
plugins from org.codehaus.mojo
are considered extra plugins. The
Apache Maven project manages the org.apache.maven.plugins
group, and
a separate independent open source community manages the Codehaus Mojo
project. If you would like to start publishing plugins to your own
groupId
, and you would like Maven to automatically scan your own
groupId
for plugin prefixes, you can customize the groups that Maven
scans for plugins in your Maven Settings.
If you wanted to be able to run the first-maven-plugin
s echo goal
by running first:echo
, add the org.sonatype.mavenbook.plugins
groupId to your '~/.m2/settings.xml as shown in
Customizing the Plugin Groups in Maven Settings. This will prepend the
org.sonatype.mavenbook.plugins
to the list of groups which Maven
scans for Maven plugins.
Customizing the Plugin Groups in Maven Settings.
<settings>
...
<pluginGroups>
<pluginGroup>org.sonatype.mavenbook.plugins</pluginGroup>
</pluginGroups>
</settings>
You can now run mvn first:echo
from any directory and see that Maven
will properly resolve the goal prefix to the appropriate plugin
identifiers. This worked because our project adhered to a naming
convention for Maven plugins. If your plugin project has an
artifactId
which follows the pattern maven-first-plugin
or
first-maven-plugin
. Maven will automatically assign a plugin goal
prefix of first
to your plugin. In other words, when the Maven
Plugin Plugin is generating the Plugin descriptor for your plugin and
you have not explicitly set the goalPrefix
in your project, the
plugin:descriptor
goal will extract the prefix from your plugin’s
artifactId
when it matches the following patterns:
-
${prefix}-maven-plugin, OR
-
maven-${prefix}-plugin
If you would like to set an explicit plugin prefix, you’ll need to
configure the Maven Plugin Plugin. The Maven Plugin Plugin is a plugin
that is responsible for building the Plugin descriptor and performing
plugin specific tasks during the package and load phases. The Maven
Plugin Plugin can be configured just like any other plugin in the
build element. To set the plugin prefix for your plugin, add the
following build element to the first-maven-plugin
project’s
pom.xml.
Configuring a Plugin Prefix.
<?xml version="1.0" encoding="UTF-8"?><project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.sonatype.mavenbook.plugins</groupId>
<artifactId>first-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>first-maven-plugin Maven Mojo</name>
<url>http://maven.apache.org</url>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>2.3</version>
<configuration>
<goalPrefix>blah</goalPrefix>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Configuring a Plugin Prefix sets the plugin prefix to blah
. If you’ve added
the org.sonatype.mavenbook.plugins
to the pluginGroups
in your
~/.m2/settings.xml, you should be able to execute the EchoMojo
by
running mvn blah:echo
from any directory.
11.4.4. Logging from a Plugin
Maven takes care of connecting your Mojo to a logging provider by
calling setLog()
prior to the execution of your Mojo. It supplies an
implementation of org.apache.maven.monitor.logging.Log
. This class
exposes methods that you can use to communicate information back to
the user. This Log
class provides multiple levels of logging similar
to that API provided by Log4J. Those
levels are captured by a series of methods available for each level:
debug, info, error and warn. To save trees, we’ve only listed the
methods for a single logging level: debug.
-
void debug( CharSequence message)
-
Prints a message to the debug logging level.
-
void debug( CharSequence message, Throwable t)
-
Prints a message to the debug logging level which includes the
stack trace from the
Throwable
(either Exception
or Error
)
-
void debug( Throwable t )
-
Prints out the stack trace of the
Throwable
(either Exception
or Error
)
Each of the four levels exposes the same three methods. The four
logging levels serve different purposes. The debug level exists for
debugging purposes and for people who want to see a very detailed
picture of the execution of a Mojo. You should use the debug logging
level to provide as much detail on the execution of a Mojo, but you
should never assume that a user is going to see the debug level. The
info level is for general informational messages that should be
printed as a normal course of operation. If you were building a plugin
that compiled code using a compiler, you might want to print the
output of the compiler to the screen.
The warn logging level is used for messages about unexpected events
and errors that your Mojo can cope with. If you were trying to run a
plugin that compiled Ruby source code, and there was no Ruby source
code available, you might want to just print a warning message and
move on. Warnings are not fatal, but errors are usually build-stopping
conditions. For the completely unexpected error condition, there is
the error logging level. You would use error if you couldn’t continue
executing a Mojo. If you were writing a Mojo to compile some Java code
and the compiler wasn’t available, you’d print a message to the error
level and possibly pass along an Exception that Maven could print out
for the user. You should assume that a user is going to see most of
the messages in info and all of the messages in error.
11.4.5. Mojo Class Annotations
In first-maven-plugin
, you didn’t write the plugin descriptor
yourself, you relied on Maven to generate the plugin descriptor from
your source code. The descriptor was generated using your plugin
project’s POM information and a set of annotations on your EchoMojo
class. EchoMojo
only specifies the @goal
annotation, here is a
list of other annotations you can place on your Mojo
implementation.
-
@goal <goalName>
-
This is the only required annotation which gives a name to this
goal unique to this plugin.
-
@requiresDependencyResolution <requireScope>
-
Flags this mojo as requiring the dependencies in the specified
scope (or an implied scope) to be resolved before it can
execute. Supports compile, runtime, and test. If this annotation
had a value of
test
, it would tell Maven that the Mojo cannot be
executed until the dependencies in the test scope had been
resolved.
-
@requiresProject (true|false)
-
Marks that this goal must be run inside of a project, default is
true
. This is opposed to plugins like archetypes, which do not.
-
@requiresReports (true|false)
-
If you were creating a plugin that relies on the presence of
reports, you would need to set
requiresReports
to true
. The
default value of this annotation is false.
-
@aggregator (true|false)
-
A Mojo with aggregator set to true is supposed to only run once
during the execution of Maven. It was created to give plugin
developers the ability to summarize the output of a series of
builds; for example, to create a plugin that summarizes a report
across all projects included in a build. A goal with
aggregator
set to true
should only be run against the top-level project in a
Maven build. The default value of aggregator
is false
.
-
@requiresOnline (true|false)
-
When set to
true
, Maven must not be running in offline mode when
this goal is executed. Maven will throw an error if one attempts to
execute this goal offline. Default: false
.
-
@requiresDirectInvocation
-
When set to
true
, the goal can only be executed if it is
explicitly executed from the command-line by the user. Maven will
throw an error if someone tries to bind this goal to a lifecycle
phase. The default for this annotation is false
.
-
@phase <phaseName>
-
This annotation specifies the default phase for this goal. If you
add an execution for this goal to a pom.xml and do not specify
the phase, Maven will bind the goal to the phase specified in this
annotation by default.
-
@execute [goal=goalName|phase=phaseName [lifecycle=lifecycleId]]
-
This annotation can be used in a number of ways. If a phase is
supplied, Maven will execute a parallel lifecycle ending in the
specified phase. The results of this separate execution will be
made available in the Maven property ${executedProperty}.
The second way of using this annotation is to specify an explicit goal
using the prefix:goal
notation. When you specify just a goal, Maven
will execute this goal in a parallel environment that will not affect
the current Maven build.
The third way of using this annotation would be to specify a phase in
an alternate lifecycle using the identifier of a lifecycle.
@execute phase="package" lifecycle="zip"
@execute phase="compile"
@execute goal="zip:zip"
If you look at the source for EchoMojo
, you’ll notice that Maven is
not using the standard annotations available in Java 5. Instead, it is
using Commons
Attributes. Commons Attributes provided a way for Java programmers to
use annotations before annotations were a part of the Java language
specification. Why doesn’t Maven use Java 5 annotations? Maven doesn’t
use Java 5 annotations because it is designed to target pre-Java 5
JVMs. Because Maven has to support older versions of Java, it cannot
use any of the newer features available in Java 5.
11.4.6. When a Mojo Fails
The execute()
method in Mojo throws two exceptions
MojoExecutionException
and MojoFailureException
. The difference
between these two exception is both subtle and important, and it
relates to what happens when a goal execution "fails". A
MojoExecutionException
is a fatal exception, something unrecoverable
happened. You would throw a MojoExecutionException
if something
happens that warrants a complete stop in a build; you re trying to
write to disk, but there is no space left, or you were trying to
publish to a remote repository, but you can’t connect to it. Throw a
MojoExecutionException
if there is no chance of a build continuing;
something terrible has happened and you want the build to stop and the
user to see a "BUILD ERROR" message.
A MojoFailureException
is something less catastrophic, a goal can
fail, but it might not be the end of the world for your Maven build. A
unit test can fail, or a MD5 checksum can fail; both of these are
potential problems, but you don’t want to return an exception that is
going to kill the entire build. In this situation you would throw a
MojoFailureException
. Maven provides for different "resiliency"
settings when it comes to project failure. Which are described below.
When you run a Maven build, it could involve a series of projects each
of which can succeed or fail. You have the option of running Maven in
three failure modes:
-
mvn -ff
-
Fail-fast mode: Maven will fail (stop) at the first build failure.
-
mvn -fae
-
Fail-at-end: Maven will fail at the end of the build. If a project
in the Maven reactor fails, Maven will continue to build the rest
of the builds and report a failure at the end of the build.
-
mvn -fn
-
Fail never: Maven won’t stop for a failure and it won’t report a
failure.
You might want to ignore failure if you are running a continuous
integration build and you want to attempt a build regardless of the
success of failure of an individual project build. As a plugin
developer, you’ll have to make a call as to whether a particular
failure condition is a MojoExecutionException
or a
MojoFailureExeception
.