5 things you didn't know about ... Apache Maven

Tips for managing the project life cycle with Maven

You might be familiar with profiles, but did you know that you can use them in Maven to execute specific behaviors in different environments? This installment in the 5 things series looks beyond Maven's build features, and even its basic tools for managing the project life cycle, delivering five tips that will improve the productivity and ease with which you manage applications in Maven.

Share:

Steven Haines, Founder and CEO, GeekCap Inc.

Steven Haines is a technical architect at ioko and the founder of GeekCap Inc. He has written three books on Java programming and performance analysis, as well as several hundred articles and a dozen white papers. Steven has also spoken at industry conferences such as JBoss World and STPCon, and he previously taught Java programming at the University of California, Irvine, and Learning Tree University. He resides near Orlando, Florida.



05 October 2010

Also available in Chinese Russian Japanese Spanish

Maven is an excellent build tool for Java™ developers, and you can use it to manage the life cycle of your projects as well. As a life-cycle management tool, Maven operates against phases rather than Ant-style build "tasks." Maven handles all phases of the project life cycle, including validation, code generation, compilation, testing, packaging, integration testing, verification, installation, deployment, and project site creation and deployment.

To understand the difference between Maven and a traditional build tool, consider the process of building a JAR file and an EAR file. Using Ant, you would need to define specific tasks to assemble each artifact. Maven, on the other hand, does most of that work for you: you just tell it whether the project is a JAR or EAR file, then instruct it to process the "package" phase. Maven will find the required resources and construct the files.

You'll find plenty of introductory tutorials for getting started with Maven, including some listed in this article's Resources section. The five tips here are intended to help you with what comes next: the programming scenarios that arise when using Maven to manage the life cycle of your applications.

1. Executable JAR files

About this series

So you think you know about Java programming? The fact is, most developers scratch the surface of the Java platform, learning just enough to get the job done. In this ongoing series, Java technology sleuths dig beneath the core functionality of the Java platform, turning up tips and tricks that could help you solve even your stickiest programming challenges.

Building a JAR file with Maven is pretty easy: just define the project packaging as "jar" and then execute the package life-cycle phase. But defining an executable JAR file is more tricky. Doing this effectively involves the following steps:

  1. Define a main class in your JAR's MANIFEST.MF file that defines the executable class. (MANIFEST.MF is the file that Maven generates when packaging your application.)
  2. Find all of the libraries on which your project depends.
  3. Include those libraries in your MANIFEST.MF file so that your application classes can find them.

You can do all of this manually, or you can do it more efficiently with the help of two Maven plug-ins: maven-jar-plugin and maven-dependency-plugin.

maven-jar-plugin

The maven-jar-plugin does many things, but here we're interested in using it to modify the contents of a default MANIFEST.MF file. In the plug-ins section of your POM file, add the code shown in Listing 1:

Listing 1. Using maven-jar-plugin to modify MANIFEST.MF
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>com.mypackage.MyClass</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

All Maven plug-ins expose their configuration through a <configuration> element. In this example, the maven-jar-plugin modifies its archive attribute and specifically the archive's manifest attribute, which controls the contents of the MANIFEST.MF file. It includes three elements:

  • addClassPath: Setting this element to true tells the maven-jar-plugin to add a Class-Path element to the MANIFEST.MF file, and to include all dependencies in that Class-Path element
  • classpathPrefix: If you plan on including all of your dependencies in the same directory as the JAR you are building, then you can omit this element; otherwise, use classpathPrefix to specify the prefixes of all dependent JAR files. In Listing 1, classpathPrefix specifies that all dependencies should be located in a "lib" folder relative to the archive.
  • mainClass: Use this element to define the name of the class to execute when the user executes the JAR file with a java -jar command.

maven-dependency-plugin

Once you've configured the MANIFEST.MF file with these three elements, your next step is to actually copy all of the dependencies to the lib folder. For this, you use the maven-dependency-plugin, as shown in Listing 2:

Listing 2. Using maven-dependency-plugin to copy dependencies to lib
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>install</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                              ${project.build.directory}/lib
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

The maven-dependency-plugin has a copy-dependencies goal that will copy your dependencies to the directory of your choosing. In this example, I copied dependencies to the lib directory under the build directory (project-home/target/lib).

With your dependencies and modified MANIFEST.MF in place, you can launch the application with a simple command:

java -jar jarfilename.jar

2. Customizing MANIFEST.MF

While the maven-jar-plugin allows you to modify common portions of a MANIFEST.MF file, there are times when you need a more customized MANIFEST.MF. The solution to this is two-fold:

  1. Define all of your custom configurations in a "template" MANIFEST.MF file.
  2. Configure the maven-jar-plugin to use your MANIFEST.MF file and augment it with any Maven customizations.

As an example, consider a JAR file that contains a Java agent. For a Java agent to run, it needs to define a Premain-Class and permissions. Listing 3 shows the contents of such a MANIFEST.MF file:

Listing 3. Premain-Class definition in a custom MANIFEST.MF file
Manifest-Version: 1.0
Premain-Class: com.geekcap.openapm.jvm.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true

In Listing 3, I've specified that the Premain-Classcom.geekcap.openapm.jvm.agent.Agent will be granted permission to redefine and retransform classes. Next, I update the maven-jar-plugin to include the MANIFEST.MF file, as shown in Listing 4:

Listing 4. Including Premain-Class
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifestFile>
                          src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>lib/</classpathPrefix>
                            <mainClass>
                              com.geekcap.openapm.ui.PerformanceAnalyzer
                            </mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

Maven 3

Maven 2 has established its place as one of the most popular and well-used open source Java life-cycle management tools. Maven 3, promoted to alpha 5 in September 2010, brings some eagerly awaited changes to Maven. See the Resources section to find out what's new in Maven 3.

This is an interesting example because it both defines a Premain-Class that allows the JAR file to work as a Java agent and has a mainClass that allows it to run as an executable JAR file. In this particular example, I've used OpenAPM (a code tracing tool that I built) to define code tracing that will be recorded by the Java agent and a user interface, which will facilitate analysis of recorded traces. In short, the example shows the power of combining an explicit manifest file with dynamic modifications.


3. Dependency trees

One of the most useful features of Maven is its support for dependency management: you simply define the libraries your application depends on, and Maven locates them (either in your local or a central repository), downloads them, and uses them to compile your code.

On occasion, you might need to know the origin of a particular dependency — such as if you were to find different and incompatible versions of the same JAR file in your build. In this case, you would need to prevent one version of the JAR file from being included in your build, but first you would need to locate the dependency holding the JAR.

Locating dependencies turns out to be surprisingly easy once you know the following command:

mvn dependency:tree

The dependency:tree argument displays all of your direct dependencies and then shows all sub-dependencies (and their sub-dependencies, and so on). For example, the Listing 5 is an excerpt from a client library required by one of my dependencies:

Listing 5. A Maven dependency tree
[INFO] ------------------------------------------------------------------------
[INFO] Building Client library for communicating with the LDE
[INFO]    task-segment: [dependency:tree]
[INFO] ------------------------------------------------------------------------
[INFO] [dependency:tree {execution: default-cli}]
[INFO] com.lmt.pos:sis-client:jar:2.1.14
[INFO] +- org.codehaus.woodstox:woodstox-core-lgpl:jar:4.0.7:compile
[INFO] |  \- org.codehaus.woodstox:stax2-api:jar:3.0.1:compile
[INFO] +- org.easymock:easymockclassextension:jar:2.5.2:test
[INFO] |  +- cglib:cglib-nodep:jar:2.2:test
[INFO] |  \- org.objenesis:objenesis:jar:1.2:test

You can see in Listing 5 that the sis-client project requires the woodstox-core-lgpl and the easymockclassextension libraries. The easymockclassextension library, in turn, requires the cglib-nodep library and the objenesis library. If I were having problems with objenesis, such as two versions, 1.2 and 1.3, then this dependency tree would show me that the 1.2 artifact was being imported indirectly by the easymockclassextension library.

The dependency:tree argument has saved me many hours of debugging a broken build; I hope it will do the same for you.


4. Using profiles

Most substantial projects have at least a core group of environments consisting of tasks related to development, quality assurance (QA), integration, and production. The challenge of managing all of those environments is in configuring your build, which has to connect to the correct database, execute the correct set of scripts, and deploy all the right artifacts to each environment. Using Maven profiles lets you do all this without having to build explicit instructions for each environment individually.

The key is in combining environmental profiles with task-oriented ones. Each environmental profile defines its specific locations, scripts, and servers. So, in my pom.xml file, I would define the task-oriented profile "deploywar" as shown in Listing 6:

Listing 6. A deployment profile
    <profiles>
        <profile>
            <id>deploywar</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>net.fpic</groupId>
                        <artifactId>tomcat-deployer-plugin</artifactId>
                        <version>1.0-SNAPSHOT</version>
                        <executions>
                            <execution>
                                <id>pos</id>
                                <phase>install</phase>
                                <goals>
                                    <goal>deploy</goal>
                                </goals>
                                <configuration>
                                    <host>${deploymentManagerRestHost}</host>
                                    <port>${deploymentManagerRestPort}</port>
                                    <username>${deploymentManagerRestUsername}</username>
                                    <password>${deploymentManagerRestPassword}</password>
                                    <artifactSource>
                                      address/target/addressservice.war
                                    </artifactSource>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

This profile, identified by the ID "deploywar," executes the tomcat-deployer-plugin, which is configured to connect to a specific host, port, and to specific username and password credentials. All of this information is defined using variables, such as ${deploymentmanagerRestHost}. These variables are defined in my profiles.xml file on a per-environment basis, as shown in Listing 7:

Listing 7. profiles.xml
        <!-- Defines the development deployment information -->
        <profile>
            <id>dev</id>
            <activation>
                <property>
                    <name>env</name>
                    <value>dev</value>
                </property>
            </activation>
            <properties>
                <deploymentManagerRestHost>10.50.50.52</deploymentManagerRestHost>
                <deploymentManagerRestPort>58090</deploymentManagerRestPort>
                <deploymentManagerRestUsername>myusername</deploymentManagerRestUsername>
                <deploymentManagerRestPassword>mypassword</deploymentManagerRestPassword>
            </properties>
        </profile>

        <!-- Defines the QA deployment information -->
        <profile>
            <id>qa</id>
            <activation>
                <property>
                    <name>env</name>
                    <value>qa</value>
                </property>
            </activation>
            <properties>
                <deploymentManagerRestHost>10.50.50.50</deploymentManagerRestHost>
                <deploymentManagerRestPort>58090</deploymentManagerRestPort>
                <deploymentManagerRestUsername>
                  myotherusername
                </deploymentManagerRestUsername>
                <deploymentManagerRestPassword>
                  myotherpassword
                </deploymentManagerRestPassword>
            </properties>
        </profile>

Deploying Maven profiles

In the profiles.xml file in Listing 7, I defined two profiles and activated them based on the value in the env (environment) property. If the env property were set to dev then the development deployment information would be used. If the env property were set to qa, then the QA deployment information would be used, and so on.

Here's the command to deploy the file:

mvn -Pdeploywar -Denv=dev clean install

The -Pdeploywar flag tells Maven to explicitly include the deploywar profile. The -Denv=dev statement creates a system property named env and sets its value to dev, which activates the development configuration. Passing -Denv=qa would activate the QA configuration.


5. Custom Maven plug-ins

Maven puts dozens of prebuilt plug-ins at your disposal, but at some point you might find yourself in need of a custom plug-in. Building a custom Maven plug-in is straightforward:

  1. Create a new project with the POM packaging set to "maven-plugin."
  2. Include an invocation of the maven-plugin-plugin that defines your exposed plug-in goals.
  3. Create a Maven plug-in "mojo" class (a class that extends AbstractMojo).
  4. Annotate the Javadoc comments for the class to define goals and for variables that will be exposed as configuration parameters.
  5. Implement an execute() method that will be invoked when your plug-in is invoked.

As an example, Listing 8 shows relevant portions of a custom plug-in designed to deploy Tomcat:

Listing 8. TomcatDeployerMojo.java
package net.fpic.maven.plugins;

import java.io.File;
import java.util.StringTokenizer;

import net.fpic.tomcatservice64.TomcatDeploymentServerClient;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

import com.javasrc.server.embedded.CommandRequest;
import com.javasrc.server.embedded.CommandResponse;
import com.javasrc.server.embedded.credentials.Credentials;
import com.javasrc.server.embedded.credentials.UsernamePasswordCredentials;
import com.javasrc.util.FileUtils;

/**
 * Goal that deploys a web application to Tomcat
 *
 * @goal deploy
 * @phase install
 */
public class TomcatDeployerMojo extends AbstractMojo
{
	/**
	 * The host name or IP address of the deployment server
	 * 
	 * @parameter alias="host" expression="${deploy.host}" @required
	 */
	private String serverHost;
	
	/**
	 * The port of the deployment server
	 * 
	 * @parameter alias="port" expression="${deploy.port}" default-value="58020"
	 */
	private String serverPort;

	/**
	 * The username to connect to the deployment manager (if omitted then the plugin
	 * attempts to deploy the application to the server without credentials)
	 * 
	 * @parameter alias="username" expression="${deploy.username}"
	 */
	private String username;

	/**
	 * The password for the specified username
	 * 
	 * @parameter alias="password" expression="${deploy.password}"
	 */
	private String password;

	/**
	 * The name of the source artifact to deploy, such as target/pos.war
	 * 
	 * @parameter alias="artifactSource" expression=${deploy.artifactSource}" 
	 * @required
	 */
	private String artifactSource;
	
	/**
	 * The destination name of the artifact to deploy, such as ROOT.war. 
	 * If not present then the
	 * artifact source name is used (without pathing information)
	 * 
	 * @parameter alias="artifactDestination" 
	 *   expression=${deploy.artifactDestination}"
	 */
	private String artifactDestination;
	
    public void execute() throws MojoExecutionException
    {
    	getLog().info( "Server Host: " + serverHost + 
    			       ", Server Port: " + serverPort + 
    			       ", Artifact Source: " + artifactSource + 
    			       ", Artifact Destination: " + artifactDestination );
    	
    	// Validate our fields
    	if( serverHost == null )
    	{
    		throw new MojoExecutionException( 
    		  "No deployment host specified, deployment is not possible" );
    	}
    	if( artifactSource == null )
    	{
    		throw new MojoExecutionException( 
    		  "No source artifact is specified, deployment is not possible" );
    	}

        ...
   }
}

In the class header, the @goal comment specifies the goal that this MOJO executes and the @phase specifies the phase in which the goal executes. Each exposed property has a @parameter annotation that specifies the alias through which the parameter will be executed, in addition to an expression that maps to a system property containing the actual value. If the property has a @required annotation, then it is required. If it has a default-value, then that value will be used if one is not specified. In the execute() method, you can invoke getLog() to gain access to the Maven logger, which, depending on the logging level, will output the specified message to the standard output device. If the plug-in fails, throwing a MojoExecutionException will cause the build to fail.


In conclusion

You can use Maven just for builds, but at its best Maven is a project life-cycle management tool. This article has presented five lesser known features that can help you become more effective at using Maven. See the Resources section to learn more about Maven.

Next up in the 5 things series will be five tips for building beautiful user interfaces in Swing, so stay tuned.

Resources

Learn

  • Develop and deploy your next app on the IBM Bluemix cloud platform.
  • "5 things you didn't know about ... : Find out how much you don't know about the Java platform, in this series dedicated to turning Java technology trivia into useful programming tips.
  • "Introduction to Apache Maven 2" (Sing Li, developerWorks, December 2006): Familiarize yourself with the fundamental skills required to work on projects built using Maven 2, starting with this developerWorks tutorial.
  • "Managing Java Build Lifecycles with Maven" (Steven Haines, InformIT.com, July 2009): This tutorial describes the basis for Maven, the architecture behind it, and how to use it to manage the build life cycles of your Java projects.
  • Maven: The Complete Reference, Edition 0.7 (Tim O'Brien, et al.; Sonatype 2010): This free online book is an excellent resource for learning Maven, including tips for using the forthcoming Maven 3. Publisher SonaType is a software vendor that develops one of the most popular Maven repository projects, Nexus.
  • Maven 3 release notes: Learn about changes to Maven in version 3, including improvements to its usability and performance.
  • The developerWorks Java technology zone: Hundreds of articles about every aspect of Java programming.

Get products and technologies

  • Maven home page: Download Maven and learn more about it from the Apache Software Foundation.

Discuss

  • Get involved in the My developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Open source
ArticleID=548779
ArticleTitle=5 things you didn't know about ... Apache Maven
publish-date=10052010