Welcome back to Mastering Maven. On the third installment of this series we'll find out how the build can be customized using plugins. Apache Maven is an extensible tool, as it's quite impossible for the development team to figure out all possible use cases and customizations ahead of time. For this reason, Apache Maven allows its behavior to be enriched by adding plugins.
Perhaps you noticed the appearance of plugins on previous posts, when the output of running a mvn command was presented on a screenshot, for example like the following one
Here we can appreciate 5 different plugins being used to apply the required transformations from source code into a JAR file. These plugins are:
These plugins are invoked in a particular sequence that we didn't specify, reason being is that these plugins are configured out of the box as part of the default behavior provided by Apache Maven. This default configuration is dictated by what's known as the Super POM, whose nature will be discussed in a future post, but for now it's sufficient to say that every build inherits behavior from the Super POM. Secondly the order of execution is dictated by another feature: the build lifecycle. As it happens, Apache Maven follows a strict sequence of steps to execute the build; these steps are known as the build lifecycle.
In simple terms, when Apache Maven executes a build it follows a series of steps in its lifecycle, each step is known as a phase. Plugins provide goals (if you're coming from Apache Ant a goal would be "equivalent" to a target) that can be bound to a phase, thus hitching a ride in the lifecycle, resulting in their execution when the given phase is triggered. The binding between goal and phase is known as an execution, which are usually given a meaningful identifier. The Super POM configures a set of plugins by default, which in turn have many of their goals bound to specific phases, all this enabling the default behavior we've observed so far without having to configure a single plugin until this moment.
Coming back to the screenshot, let's decode the information provided by the log. The first line states "Scanning for projects...". So far we've run a simple project, not just because it has a single Java source file but because it's configuration is comprised of a single pom.xml file. Apache Maven supports building multiple projects at once as long as all of them belong to the same logical unit (in Apache Maven terms known as a reactor). We'll keep working with a simple project for the time being and will come back to multi-project builds at a later stage in the series.
Next we get a banner of sorts, where the first line states the group and artifact id of the GAV coordinates, and the second states the artifact id and version of the GAV coordinates. This banner will appear for every project in the build, in our we have a single project thus we get the banner once.
What follows is the output of each plugin execution. In the case of the clean phase, we get one plugin execution whose id is "default-clean"; this execution is bound to the "clean" goal of the maven-clean-plugin whose version happens to be 2.5. Once more, this configuration is provided by the Super POM. The other plugin invocations follow the same format, I bet you can deduct the information for each plugin execution.
Great, we now know a bit more about the build lifecycle and its phases, plugin goals and executions, and that there's a set of plugins configured by default. But, how do we configure more plugins? Or is it possible to update the configuration for existing plugins? Fear not, that's exactly what we'll cover right away.
You may have noticed that the single Java source file we've seen contains a main method, like this
Let's figure out a way to execute that method as part of the build, we'll need some kind of executable plugin. Apache Maven plugins are scattered all around the web, a good place to start our search would be MojoHaus. This site contains a list of popular and most commonly used plugins, however, it's not exhaustive. Luckily for us, the exec-maven-plugin is exactly what we're looking for. Based on the information found at the usage page for this plugin we only have to update our POM file to look like this
Now we can invoke the exec:java goal and check the output. Make sure you execute this goal after verifying the project otherwise the exec:java goal will fail stating that it can't find the compiled classes. You should get an output similar to the following one if everything went according to plan.
Good. Putting together what we know so far, the plugins block defines which plugins should be applied to a build. Each plugin block requires GAV coordinates to locate the plugin code, which tells us that plugins are resolved from repositories just like regular dependencies are. Plugins have a configuration block that is plugin specific, in the case of the exec plugin we had to specify the fully qualified class name of the main class.
Now to update the configuration of an existing plugin, say the maven-jar-plugin, let's add an additional entry to the JAR manifest. Based on what we saw on previous executions and what we know about plugin configuration we'd need to find appropriate GAV coordinates for the maven-jar-plugin; remember the remote repository search sites we saw on the previous post? Now is the time to use them again. At the time of writing the latest version of maven-jar-plugin is 3.2.0 which happens to be one minor release ahead of the version that's configured by the Super POM we currently inherit from. Here we have the opportunity to keep the old version or applying the latest, while at the same time updating the default configuration. Let's go with the latter option, thus our POM file should look like this
Alright, executing mvn verify one more time should generate the JAR file once again.
Would you look at that, the execution of the default jar goal now lists 3.2.0 as the latest version of the maven-jar-plugin, good. After this we can inspect the contents of the manifest by expanding the archive and having a look at the META-INF/MANIFEST.MF entry
Great, our custom X-Foo entry was successfully added to the JAR's manifest. One last thing, you may have noticed that for building the project we simply invoke mvn verify whereas to run the main class we have to invoke exec:java. If you've been paying attention you may have discovered that verify is a phase while exec:java is an specific goal, this is why lots of things happen when verify is invoked but just one when exec:java is called.
We've covered plenty of ground today and we'll come back to some of the topics that we left unresolved. From now on, all source code is fully available from a Git repository, one sub directory per post, such as 03-plugins.
Photo by Sharon McCutcheon.