Maven: Trove of TipsWhen I looked at Maven the first time, I thought it was an overblown and complex tool that would never replace Ant. So far, Ant was doing its job quite well and I thought, "Why should I bother to delve into an unknown tool when there's no immediate need?" How little did I know.
Nevertheless, projects like AspectWerkz, XDoclet and others were using Maven, and I was still wondering if they would know something I did not. So when I started my JDoppio project and started writing my Ant build script, I eventually got this deja vu feeling of doing the same thing over and over again. Figuring there was nothing to lose, I decided to use Maven as a test. After downloading Maven and checking out its web site, I started to write my project object descriptor, project.xml, and after 15 minutes I could compile and test my project. Since then, I've used Maven to build my project and never looked back. So far, I can realize any requirements that the project might require, no matter how advanced they are -- some of which I could have hardly done with Ant.
Many developers responded to my success by saying that they would like to use Maven, but do not like to change from a well-known tool and enter uncharted territory. I have to admit that using Maven needs some mind bending, and handing over to Maven some control that was previously held by the developer, but in my honest opinion, it is worthwhile. Still, I recommend playing around with Maven before migrating a project, because you need some understanding of how Maven works and how to go along with Maven to get the most out of it.
This article will show some of the tips and tricks I figured out with JDoppio. Hopefully, I can give you a hint how to proceed and where to get help. In the end, Maven is all about helping you, fellow developers and users, to save time and money building projects, project web sites, and distributions. But this requires that you know how to use Maven in a way that fits your project best.
The project object model, or POM, is the heart of the Maven build tool, even though it is the smallest part of the tool. Here I do not want to give a full description of the POM; instead, I want to focus on what will be important later on. The POM file begins with general project information, which is mostly used to generate the project web site. After that, the POM allows you to specify different versions of a project based on CVS tags. This allows you to create release branches and keep them separate from the main development. After listing the developers and the distribution license, the POM defines the dependencies of the project on other archives and tools. This section looks like this:
<dependencies>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.6.1</version>
<properties>
<server.lib>true</server.lib>
</properties>
</dependency>
...
</dependencies>
Maven creates a local repository under ~/.maven/repository that is used to store archives from a remote site, as well as archives generated and installed locally. Its layout looks like this:
repository
+-- <group id>
+-- <type>s
+-- <artifact id>-<version>.<type>
...
In the POM outlined above it would try to find the archive
~/.maven/repository/ant/jars/ant-1.6.1.jar, because the
type is set to .jar by default. If this archive could not be found
locally, it would try in the current remote repository with a relative URL of /ant/jars/ant-1.6.1.jar. You can also browse the remote repository if you need to figure out the group
and artifact IDs and the support version of an archive. Note that a
group can contain more than one archive with different names and
versions. In addition, you can add properties to a particular
archive in the dependency list which can be used later to identify
archives you need for a particular purpose, and we will use this later.
In the example above, the Ant 1.6.1 dependency has a property
named server.lib with the value true.
After that, the build element describes what is built and how it is tested:
<build>
<sourceDirectory>
src/java
</sourceDirectory>
<unitTestSourceDirectory>
test/src/java
</unitTestSourceDirectory>
<unitTest>
<includes>
<include>**/*Test.java</include>
</includes>
</unitTest>
<resources>
<resource>
<directory>
${basedir}/src/resource/logging</directory>
<includes>
<include>log4j.xml</include>
</includes>
</resource>
</resources>
</build>
First, this specifies the source directory -- Maven will try to compile all of the classes in this directory and its subdirectories. Then the unit test source directory specifies where the jUnit tests can be found. The unit test element then lets you specify which classes are test cases and should be executed when Maven tests the project. Finally, you can specify resources needed to build distributions, etc. Note: a complete documentation of the POM can be found on the Maven site.
If everything is set up, the source and jUnit test cases are in place, then you can compile and test your project by just entering:
maven jar:jar
on the command line. If you are offline, you can try to run Maven with
the -o option so that it will not try to download archives from the
remove repository; however, in this case, a build could fail for want
of a needed archive. Typical Maven output appears as follows:
$ maven
__ __
| \/ |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \ ~ intelligent projects ~
|_| |_\__,_|\_/\___|_||_| v. 1.0-rc3
BUILD SUCCESSFUL
Total time: 1 seconds
Finished at: Tue Jun 22 13:44:36 PDT 2004
Having finished the first step of setting up a Maven project, we are ready to look at some of the advanced features of Maven.
When Maven is executed, it will start with the given goal. First, it
will execute what is specified to be run before the goal is executed,
then the goal itself, and finally, what is specified to be executed after
the goal. Maven comes with a ton of goals ready to be used; these can be
viewed with maven -g, which produces output that looks like this:
$ maven -g
__ __
| \/ |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \ ~ intelligent projects
|_| |_\__,_|\_/\___|_||_| v. 1.0-rc3
Available [Plugins] / Goals
===========================
genapp .........................
Generate Application based on a template
html2xdoc ......................
Generates XDoc documentation from normal
HTML files
jdiff ..........................
generate an api difference report between
versions
junitdoclet ....................
Generate unit tests
[announcement]
Generate release announcement
generate .......................
Generate release announcement
...
[java]
( NO DEFAULT GOAL )
compile ........................
Compile the project
jar ............................
Create the deliverable jar file.
jar-resources ..................
Copy any resources that must be present in
the deployed JAR file
prepare-filesystem .............
Create the directory structure needed to
This listing indicates that in order to compile the project, you have to execute
Maven with maven java:compile, and to create a .jar file, you execute
maven java:jar. (Note: this goal is deprecated and
you should now use jar:jar instead.) Of course, most of these goals
also have other goals to be execute before or after the goal
itself. For example, the goal java:compile will have to execute the goal
java:prepare-filesystem beforehand to make sure the
necessary directory structure is in place.
You may wonder where these goals and their dependencies are coming from. You can easily figure it out by looking in the local plugin directory of Maven, in the ~/.maven/cache directory. There you will find a directory such as maven-java-plugin-1.4 containing a POM as well as the project.jelly file:
<project
xmlns:j="jelly:core"
xmlns:ant="jelly:ant"
xmlns:define="jelly:define"
xmlns:maven="jelly:maven">
<goal name="java:prepare-filesystem"
description="Create the directory
structure needed to compile">
<ant:mkdir dir="${maven.build.dest}"/>
</goal>
<goal name="java:compile"
description="Compile the project"
prereqs="java:prepare-filesystem">
<ant:echo>
Compiling to ${maven.build.dest}
</ant:echo>
<j:choose>
<j:when test="${sourcesPresent == 'true'}">
<ant:javac
...
This file is a Jelly Script file, with the project element specifying all additional Jelly tag libraries it includes, along with the namespace of these libraries. Even though you could omit the constant specification of the namespace -- <mkdir
...> instead of <ant:mkdir ...> -- I think it is
good practice to include namespaces, for documentation as well as
the clarity of the script.
Further on in the code, we find the goal
java:compile, with the prerequisite
java:prepare-filesystem. The body of this goal specifies how
the project is compiled. In this body, Ant and Jelly tags are
used to compile the Java code. As you probably guessed, Jelly tags
check to see if Java source is present and otherwise omits the
compilation. This scripting ability gives Maven a big advantage over
Ant, even though Maven is meant not to compete with Ant, but rather utilize it
when possible.
But what do you do when there is no goal available to do what you need? In this case, you can write your own extension, which will be called maven.xml and located in the same directory as the POM. This file looks the same as the one above and more or less does the same thing, except that it is private to the project. In this file, you can do one of the following:
Warning: you may be tempted to create a ton of scripts to do whatever you need. I recommend searching the Maven plugins first to see if you can find a plugin to do what you need. Not only will this make your file smaller and easier to maintain, but the plugin will also be updated over time, freeing you to focus on your own development. In addition, looking at these plugins can teach you a lot about using Maven.
A simple Maven extension looks like this:
`<project default="jdoppio:build"
xmlns:j="jelly:core"
xmlns:maven="jelly:maven"
xmlns:deploy="deploy"
>
<goal name="jdoppio:build"
description="Build all JDoppio
components">
<maven:reactor basedir="${basedir}"
includes="**/project.xml"
goals="jar:install"
banner="Installing:"
ignoreFailures="false" />
</goal>
...
This script defines the project's own build goal: jdoppio:build, better expressing what it does than a
simple java:compile. Inside, it is using Maven's reactor to execute
several sub-projects using the jar:install goal of each, which
compiles and tests the projects. jdoppio:build then
builds a distribution and copies into the local repository. The last step is important because other
sub-projects depend on it, and will use the POM's dependencies to use
these archives from the local repository.
|
Another way to customize your Maven project is to use the properties of Maven and its plugins. Whereas goals describe how and in what order scripts are executed, properties customize the environment, sources, and targets. Goals and properties go hand in hand, and you will find the description of goals along with the properties on a plugin web site such as that of the .war plugin.
Note: I would recommend that you only change properties when absolutely necessary, and keep the pre-set properties of Maven whenever possible. This will reduce your workload and keep various projects similarly structured.
Maven properties are first set by a list of properties files, in which the
last definition wins (please check out Maven's web site for more
documentation). Afterwards, plugins can set values but are not allowed
to override them, meaning your settings will prevail. Since this
also applies to your extension script, I recommend setting values in a
properties file. Finally, any properties set with the -D
option when Maven is started will override any previously set values.
This should be enough introduction for us to start looking into various tips and tricks how to use Maven extensively, so let's begin.
To test class loaders, I had to create multiple archives
with the same fully qualified class name but with different code. This
meant my code could not be compiled in one step, as Maven does by default. The
goal to run the test is test:test, and so I needed to
create the archive first. To do this, maven.xml contains a piece of code that looks like
this:
<preGoal name="test:test">
<attainGoal
name="build-test-archives"/>
</preGoal>
<postGoal name="test:test">
<attainGoal name="do-cleanup"/>
</postGoal>
<!--AS NOTE: Build the various archives
needed for the unit tests -->
<goal name="build-test-archives">
<!-- Do the magic here -->
</goal>
...
To make things easier to read, I normally create private goals instead of putting the code into
the preGoal tag, but that is up
to you. Inside of the private goal, I compile the code and create the
archives. This is basically good old Ant scripting:
<ant:mkdir
dir="${maven.build.dir}/test-classes/base"/>
<ant:javac
destdir="${maven.build.dir}/test-classes/base"
debug="on"
srcdir="${basedir}/test/src/java.base"
>
<ant:classpath>
<ant:pathelement
path="${maven.build.dest}"/>
<ant:pathelement
path="${maven.build.dir}/../../util/target/classes"/>
</ant:classpath>
</ant:javac>
<ant:mkdir
dir="${maven.build.dir}/test-archives"/>
<ant:jar
jarfile=
"${maven.build.dir}/test-archives/test.base.jar"
basedir=
"${maven.build.dir}/test-classes/base"
>
<ant:manifest>
<ant:attribute
name="Built-By" value="${user.name}"/>
<ant:attribute
name="Created-By" value="maven"/>
<ant:attribute
name="Package" value="${pom.package}"/>
</ant:manifest>
<ant:include name="**/*.class"/>
<ant:exclude
name="**/ReferenceClass.class"/>
</ant:jar>
With JDoppio, I wanted to do the opposite of JBoss' habit of making
everything so tightly integrated that sub-projects cannot be used by
themselves. Therefore, I wanted to create sub-projects that could be
built without building the entire project, but still able to build the
entire project in one step. Maven's reactor tag enables
the developer to call a specified goal on every project found, identified by
a POM, and so new projects can be added later on without breaking
the build. The reactor is defined in the
maven.xml file as part of another goal:
<goal name="jdoppio:clean"
description="Clean all JDoppio
components" prereqs="clean">
<maven:reactor basedir="${basedir}"
includes="**/project.xml"
goals="clean:clean"
banner="Cleaning:"
ignoreFailures="false" />
</goal>
This goal calls the clean:clean goal on every POM found in the
project's subdirectories, so that the next time the project is built, it
will be created from scratch. Note: because the prerequisite
of the goal itself is the goal clean, the main project's own
clean goal is executed first. There is also a multi-project plug in
available, but this is for generating a project web site
composed of multiple sub-projects.
The sub-projects would not be of much help if they couldn't be used within other sub-projects. This can be done in multiple ways. A project could copy the generated file from another subproject. Or it could copy its generated archives to a directory in the main project. In my case, I used the local repository because with that, I could specify the dependency in the POM with an explicit version. The only drawback is that the sub-projects that another project depends upon must be built beforehand locally at least once, otherwise it will fail. The dependency looks like this:
<dependency>
<groupId>jdoppio</groupId>
<artifactId>jdoppio.loader</artifactId>
<version>0.1</version>
</dependency>
This dependency states that an archive is looked for in jdoppio/jars with the name jdoppio.loader-0.1.jar. To have an archive like this available in the local repository, the subproject POM must look like this:
<project>
...
<id>jdoppio.loader</id>
<name><what ever you like></name>
<groupId>jdoppio</groupId>
...
A nice feature of Maven is that its POM is accessible from a Jelly
script, and so it is possible to loop through the dependencies and
check them out. In the .war plugin it is possible to indicate that an
archive has to be added to the .war file (into the
/WEB-INF/lib directory) by adding a
war.bundle property like this:
<dependency>
<groupId>jdoppio</groupId>
<artifactId>jdoppio.loader</artifactId>
<version>0.1</version>
<properties>
<war.bundle>true</war.bundle>
</properties>
</dependency>
I was facing the problem of copying some of the archives in the POM's dependencies list to library directory of a test server installation, to run the server standalone. So the script loops through the dependencies, looks for a particular property, and checks the type of the dependency, and when everything is okay, it will copy it:
<!-- Loop through all artifacts -->
<j:forEach var="lib"
items="${pom.artifacts}">
<j:set var="dep"
value="${lib.dependency}"/>
<!-- Check if a certain property is set
correctly and that the archive is of the
correct type -->
<j:if
test="${dep.getProperty('server.lib')=='true'}">
<j:if test="${dep.type =='jar'}">
<!-- Now we can copy this archive -->
<ant:copy
todir="${maven.build.dir}/test-server/lib"
file="${lib.path}"/>
</j:if>
</j:if>
</j:forEach>
After being told that JAXB archives can be freely distributed, I needed
to add them into the project directory and add them to a class path, so
that JAXB could generate the XML binding classes and compile
these classes afterwards. One of the Jelly's Ant tags is a File
Scanner, which allows you to obtain an iterator over a list of files
and then use them within your maven.xml file. In the Maven tag library,
the tag addPath allows you to add a path to an existing
path. So here I loop over the /lib directory and add them
to a class path. Note: I ran into a problem with using
dots in the name of the class path which could only be resolved after
omitting the dots (I guess Maven is treating this as an attribute).
<!-- Note the capital S because it seems to be
case sensitive here -->
<ant:fileScanner var="ownLibs">
<ant:fileset dir="${basedir}/lib">
<ant:include name="*.jar"/>
</ant:fileset>
</ant:fileScanner>
<!-- A file scanner provides and iterator so we
this one in the forEach to loop over the
directory -->
<j:forEach var="entry"
items="${ownLibs.iterator()}">
<!-- Create a path to be added afterwards
to the dependency class path -->
<ant:path id="libEntryPath"
path="${entry}"/>
<maven:addPath
id="maven.dependency.classpath"
refid="libEntryPath"/>
</j:forEach>
Also note that I use the maven.dependency.classpath property to
add the archives. The reason to do so is that the Java plugin is using
this as the class path to compile the project's Java classes, which is
necessary because the project is using JAXB classes such as JAXBException. This is an example of customizing Maven rather than scripting the
compilation yourself, which saves you time and money. In the case of using
your own path, you need to create it up front with <and:path
id="<Your Name>"/>, because the path has to
exist when used in addPath.
I converted this code into a Maven plugin that can be downloaded from my weblog. This plugin comes with the current JAXB archives and does not need any additional archives to be downloaded.
In the Catalina service, I wanted to automate the extraction of the Catalina distribution, but only do so if this had not already been done. So I needed to figure out whether the directory already existed:
<-- Create a property with a default value so
that property is set even when directory is
not available -->
<j:set var="catalinaPresent"
value="false"/>
<-- Line below is wrapped to fit the page -->
<util:available file=
"${maven.build.dir}/../../../../
server/target/test-server/catalina"
>
<-- Directory is available not set the
property value to true -->
<j:set var="catalinaPresent"
value="true"/>
</util:available>
<-- If property is not set to true so we need
to extract Catalina -->
<j:if
test="${catalinaPresent != 'true'}">
<ant:unzip
...
Maven can create a project web site with quite a number of reports in it -- that's the reason so many project web sites look similar. Again by default Maven is using its own template to create the web site, but this can be customized by providing a navigation.xml file in the /xdocs directory of your project. In my case, I wanted to include the sub-projects as well as additional information to the project web site. This file can look like this:
<project name="JDoppio">
<title>JDoppio: The Next Generation
Application Server</title>
<body>
<links>
<item name="Maven"
href="http://maven.apache.org/"/>
</links>
<menu name="Definition">
<item name="Download"
href="download.html"/>
...
</menu>
<menu name="Projects">
<item name="Utility Project"
href="./multiproject/jdoppio.util/index.html"/>
...
</menu>
</body>
</project>
First, I added a link back to Maven as a tribute to them. Then I created
a menu, Definition, with links to other web pages, such as
the download instruction page shown here. Afterwards, in the
Projects menu, I added a link to a subproject, Util, so that the user can easily navigate to the project
web site of this subproject. Within the /xdocs directory,
all pages are written in the xdoc format with an
.xml extension and the XDoc plugin will generate
HTML pages from them. In JDoppio I am using the Multi-Project plugin to generate the entire project
web site, because the project is composed of multiple sub-projects.
Every subdirectory is placed into the common /multiproject
directory and therefore, a link to a subproject in navigation page is
just relative to the /multiproject directory.
The best documentation and help for Maven I've found so far are the Maven plugins. They are a great source of ideas on how to use Maven, even though it is sometimes tough to distinguish between the old and new styles. It also helps to check out the tag library documentation on the Jelly web site if you need to know more about Jelly and its tags. The Maven Jelly tags can be found on the Maven web site. You could also search the web for projects using Maven, like my JDoppio project, and check out how they are building their projects.
I like Maven because it lets me focus on what I want to accomplish and because I can avoid dealing with nasty build scripts that Ant makes me write over and over again. So far, I have not run into any hurdles that would make me question my decision to use Maven. Nevertheless, I do recommend starting slowly with Maven and letting your knowledge and confidence grow with it. I would like it if Ant would provide a Maven task, so that developers could gradually migrate their projects from Ant to Maven.
Finally, my last tip: let Maven run the project and limit yourself to steering Maven instead of controlling it. In Ant, developers control the build process with their scripts, but in Maven, there is a building process already in place, so it is much easier to go with it. Most of the time, the solution is just around the corner and only needs to be utilized the right way.
Have fun!
Andreas Schaefer is a system architect for J2EE at SeeBeyond Inc., where he leads application server development.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.