Apache Maven Part 2 - Build lifecycle and Apache Maven Plugins

This multi-post series is looking at Apache Maven. In the second part, we will take a closer look at the build lifecycle and the workers in Maven - Maven Plugins.


This post is part of a series about Apache Maven.
You can read part one - an introduction to Maven here.
You can read part three - Maven inheritance and aggregation here.


Introduction

As mentioned in the first post, the idea of Maven was to solve the issue of different build results on different machines. That is why Maven is based around the concept of a build lifecycle and the process for building and distributing a project is clearly defined.
There are three built-in lifecycles, and each of those lifecycles consists of multiple phases, where each phase represents a different stage in the lifecycle.

Lifecycles and phases

The lifecycles and lifecycle phases are:

  • default - handles the project deployment

    • validate - validate the project is correct and all the necessary information is present
    • initialize - initialize the build state, e.g. set variables and create directories
    • generate-sources - generate any source code to include in the compilation
    • process-sources - process the source code
    • generate-resources - generate resources to include in the package
    • process-resources - copy and process the resources into the destination directory so it is ready for packaging
    • compile - compile the source code of the project
    • process-classes - post-process the generated files from compilation
    • generate-test-sources - generate any test source code to include in the compilation
    • process-test-sources - process the test source code
    • generate-test-resources - create resources for testing
    • process-test-resources - copy and process the resources into the test destination directory
    • test-compile - compile the test source code into the test destination directory
    • process-test-classes - post-process the generated files from test compilation
    • test - run tests using a suitable test framework. These tests should not require the code to be packaged or deployed
    • prepare-package - perform any operations necessary to prepare a package before the actual packaging. This often results in an unpacked, processed version of the package
    • package - take the compiled code and package it in its distributable format, such as JAR
    • pre-integration-test - perform actions required before integration tests are executed
    • integration-test - process and deploy the package if necessary into an environment where integration tests can be run
    • post-integration-test - perform actions required after integration tests have been executed. This may include cleaning up the environment
    • verify - run any checks to verify the package is valid and meets the criteria
    • install - install the package into the local repository, so it can be used as a dependency in other projects locally
    • deploy - done in an integration or release environment, copies the final package to the remote repository so it can be shared with others
  • clean - handles the cleaning of the project

    • pre-clean - execute processes that are needed before the actual project cleaning
    • clean - removes all files generated by the previous build
    • post-clean - execute processes that to finalize the clean lifecycle
  • site - handles the creation of the documentation

    • pre-site - execute processes that are needed before the actual project site generation
    • site - generate the project’s site documentation
    • post-site - execute processes needed to finalize the site generation and to prepare for deployment
    • site-deploy - deploy the generated site documentation to the specified web server

We can execute phases by calling mvn {phase}, for example:

mvn compile

When a lifecycle phase is executed, the phases run sequentially up to and including the selected phase to complete the command.
The most well-known and most-used build phases are clean, package and deploy.
Phases, however, do not perform the work needed to complete the build. The actual workhorse of Maven are plugins with their plugin goals.

Plugins and plugin goals

As mentioned, Maven Plugins are the component that does the actual work that needs to be done to complete the build. Plugins execute specific tasks that are represented as plugin goals and each plugin goal can be bound to a specific phase to define when the task should be executed.

To recap, plugins are artifacts that provide goals to Maven, and each plugin can have multiple plugin goals. For example, the Compiler plugin has the plugin goals compile and testCompile, which are bound to the compile and testCompile phases in the default lifecycle respectively.

From this information we can conclude that phases are abstract - they do not execute any action by themselves, but rather they represent a point in time of the build. Each phase has zero or more plugin goals bound to it that execute some action, and if a phase has no plugin goals bound to it then the phase is skipped since nothing needs to be executed.

Some plugin goals can also be executed directly from the command line, such as the analyze and tree goals from the maven-dependency-plugin.
These can be triggered directly from the command line by calling

*mvn dependency:analyze* 

or

*mvn dependency:tree*  

Since we mentioned that plugin goals need to be bound to the lifecycle phases, let us look at that next. To bind a plugin goal to a lifecycle phase, we have two options:

  1. Specify the packaging of the Maven project in the pom file

    Each packaging has a list of goals it binds to a specific phase by default.
    For example, the default jar packaging binds the plugin goal compiler:compile to the compile phase, the jar:jar plugin goal to the phase package, and others.
    You can find the full list of the default bindings in the lifecycle-documentation.

  2. Define plugins in the plugin section

    The second way to add plugin goals to your build is to configure plugins.
    When configuring plugins, you need to provide information about the phase you want to bind to and the plugin goal to execute. The goals you specify will then be added to the goals that are already bound to the phase from the type of packaging.
    If there are multiple goals bound to the same phase, they will be executed in the order they appear in the POM file, but only after all the goals from the packaging are run.

    An example of a simple Maven project with a plugin configuration might be something like:

    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.devflection</groupId>
    <artifactId>maven-plugin-configuration-example</artifactId>
    
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <execution>
                        <phase>clean</phase>
                        <configuration>
                            <target>
                                <echo>Hello world from the antrun plugin!</echo>
                            </target>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    

    This plugin will run in the clean phase, and it will trigger an ant echo task that will print out the message.

We can browse the available plugins on the Maven plugin repository.
All the official Maven plugins are named maven-{pluginName}-plugin, while the unofficial ones are named {pluginName}-maven-plugin or any other name.
If we do not find a plugin that fits our needs, there is always the option of creating our own plugin and we will look at how to next.

Creating your own Maven plugin

Plugins are java projects, that contain plugin goals in the form of MOJOs (Maven plain Old Java Objects).
Each of the MOJOs represents one plugin goal, and as the name suggests they are Java objects that extend the AbstractMojo class and implement the execute method.
A simple example of a custom plugin MOJO that emits a hard-coded message:

package com.devflection.plugin;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;

/**
 * Says "Hello world from our own maven plugin!"
 *
 */
@Mojo( name = "helloworld")
public class HelloWorldMojo extends AbstractMojo {

    public void execute() throws MojoExecutionException {
        getLog().info("Hello world from our own maven plugin!");
    }

}

To use the plugin, we need to build it, so we need need to configure the POM file.
The important part is specifying the packaging as maven-plugin, and the naming of our artifact.
The recommended naming scheme is {our_plugin_name}-maven-plugin, which also automatically shortens the command line to directly execute your plugin.
Be careful not to name your maven plugin maven-{our_plugin_name}-plugin, since this is reserved for official Maven plugins and is an infringement into their trademark.

This is the pom configured for the previous MOJO that allows us to build a plugin.

<project>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.devflection.plugin</groupId>
    <artifactId>hello-world-maven-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>maven-plugin</packaging>

    <name>Our first Maven Plugin</name>

    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.0</version>
        </dependency>

        <!-- dependencies to annotations -->
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

After the POM file is configured we need to actually create the artifact and install it locally. We can achieve this by running from the project base folder:

mvn install

Now that our plugin is built and installed in our local Maven repository, we can also execute it. To achieve this, we need to add it to a calling project’ POM file, like this:

<build>
    <plugins>
        <plugin>
            <groupId>com.devflection.plugin</groupId>
            <artifactId>hello-world-maven-plugin</artifactId>
            <version>1.0-SNAPSHOT</version>      
        </plugin>
    </plugins>
</build>

And then we can execute the plugin directly from the command line by calling mvn groupId:artifactId:version:goal, or in our case

mvn com.devflection.plugin:hello-world-maven-plugin:1.0-SNAPSHOT:helloworld

This is a long command, and since we followed the recommended naming scheme we can benefit from automatic shortening and call it like

mvn hello-world:helloworld

Our sample plugin has no properties or configurations so it only prints the hard-coded message to the log.
But you can imagine, that we also want to extend the plugins and make them configurable. This is done by adding properties to the class and annotating them.
For example, we can make the message configurable by making the message a property of the class and annotating it so we can inject the content from the plugin configuration.

package com.devflection.plugin;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;

/**
 * Says a custom message that is passed to it or 
 * "Hello world from our own maven plugin!"
 */
@Mojo( name = "helloworld")
public class HelloWorldMojo extends AbstractMojo {  

    /**
     * The message to display.
     */
    @Parameter( property = "helloworld.message", defaultValue = "Hello world from our own maven plugin!" )
    private String message;

    public void execute() throws MojoExecutionException {
        getLog().info(message);
    }
}

By doing this we can configure a custom message by setting the property in the plugin configuration in our project POM file like this:

<plugin>
    <groupId>com.devflection.plugin</groupId>
    <artifactId>hello-world-maven-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
    <configuration>
        <message>
            Three little birds
            pitch by my doorstep 
            singin' sweet songs 
            of melodies pure and true 
            sayin', this is my message to you
        </message>
    </configuration>
</plugin>

Now if we run the command mvn hello-world:helloword again, it should show the new message.

Conclusion

As we saw, Maven Plugins are the actual workers behind the scenes in Maven. There are a lot of default plugin goals bound when we specify a packaging type for our project, but we can also include more plugins by configuring them in the build section of our POM file.
If needed, we can also create our own plugin that we can deploy on the Maven plugin repository and share it with others.
All in all, we can see that Maven plugins are a very powerful and flexible tool that allows us to configure our build process exactly the way we want it to be.

As always, the full project is on GitHub.


This is it for our Maven plugins post.
Thank you for reading through, I hope you found it useful and be sure to check out the next post, where we will find out more about multi-module projects and managing that complexity with Maven.


java  maven