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.
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.
Example 11.2. 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 import 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.
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
Mojoimplementation 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 thesetLog()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 yourMojois executed, and yourMojocan retrieve the logging object by callinggetLog(). Instead of printing out status to Standard Output or the console, yourMojois going to invoke methods on theLogobject. 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. Example 11.3, “A Simple EchoMojo” shows a trivial
Mojo implement which simply prints out a message
to the console.
Example 11.3. 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".
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.
Example 11.4, “Maven Metadata for the Maven Plugin Group” shows a snippet of the
maven-metadata-central.xml file from the
org.apache.maven.plugin group.
Example 11.4. 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 Example 11.4, “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 Example 11.5, “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.
Example 11.5. 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.
Example 11.6. 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>
Example 11.6, “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.
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(eitherExceptionorError) void debug( Throwable t )-
Prints out the stack trace of the
Throwable(eitherExceptionorError)
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.
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
requiresReportstotrue. The default value of this annotation is false. - @aggregator (true|false)
-
A Mojo with
aggregatorset totrueis 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 withaggregatorset totrueshould only be run against the top-level project in a Maven build. The default value ofaggregatorisfalse. - @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 isfalse. - @phase <phaseName>
-
This annotation specifies the default phase for this goal. If you add an execution for this goal to a
pom.xmland 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:goalnotation. 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.
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.
