Maven Recipe: Building an aggregate jar

Maven builds follow the “one project, one artifact” rule. This means that although it’s possible to build more than one artifact from a Maven project,  it’s not a good idea. It also means that while it’s also possible to build one artifact from multiple projects, it’s not entirely straightforward, and that’s what this post is about.

When refactoring a monolithic build into a modular one, often the downstream consumers are not prepared to consume multiple artifacts, so the build still needs to create a single jar with all the classes. This type of change is what I like to call build refactoring, similar to code refactoring. If code refactoring deals in restructuring an existing body of code, build refactoring aims to restructure an existing build.

Assuming that we have split a large project into multiple modules:

This high-complexity project used to contain two classes, but since they were unrelated we decided to split each into its own module.

When running mvn package, each module creates its individual jar:

robert@neghvar:~/workspace/aggregate-jar> mvn clean package                                                             
[INFO] Scanning for projects...  
(snip...)
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------------------
[INFO] Maven Recipe: Aggregate jar ........................... SUCCESS [1.641s]
[INFO] Maven Recipe: Aggregate jar - first module ............ SUCCESS [2.066s]
[INFO] Maven Recipe: Aggregate jar - second module ........... SUCCESS [0.697s]
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Sat May 15 00:59:01 EEST 2010
[INFO] Final Memory: 23M/251M
[INFO] ------------------------------------------------------------------------

Inspecting the resulting jar files confirms this assertion:

robert@neghvar:~/workspace/aggregate-jar> find -name \*.jar
./aggregate-first-module/target/aggregate-first-module-1.0.0-SNAPSHOT.jar
./aggregate-second-module/target/aggregate-second-module-1.0.0-SNAPSHOT.jar

Of course, we can always use a custom shell command, or an ant file, or even the maven-antrun-plugin to combine the two jars. While easy to do correctly and even in a cross-platform manner,  the downside is that we lose the simplicity and modularity of a pure Maven build.

Following the “one project, one artifact” rule, it becomes clear that the solution is to add another project, which generates the distribution jar. We will add a new aggregate-dist module to the build, which will use the maven-assembly-plugin to combine the all resulting classes into a single jar. The module section therefore becomes

While the assembly plugin is well known and used, there are a few considerations which must be observed when aggregating the results of a multi-module build.

The distribution module should list the modules to assemble as dependencies:

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aggregate-first-module</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aggregate-second-module</artifactId>
<version>${project.version}</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
 </dependencies>

The assembly plugin should be bound to the package lifecycle phase, and invoke the ‘single’ goal:

<plugin>

 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-assembly-plugin</artifactId>
 <version>2.2-beta-5</version>
 <executions>
 <execution>
 <id>package-all</id>
 <phase>package</phase>
 <goals>
 <goal>single</goal>
 </goals>
 <configuration>
 <descriptors>
 <descriptor>src/main/assembly/all-jar.xml</descriptor>
 </descriptors>
 </configuration>
 </execution>
 </executions>
 </plugin>

The assembly descriptor should include the unpacked non-transitive dependencies:


<assembly
 xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
 <id>all-jar</id>
 <formats>
 <format>jar</format> <!-- the result is a jar file -->
 </formats>

 <includeBaseDirectory>false</includeBaseDirectory> <!-- strip the module prefixes -->

 <dependencySets>
 <dependencySet>
 <unpack>true</unpack> <!-- unpack , then repack the jars -->
 <useTransitiveDependencies>false</useTransitiveDependencies> <!-- do not pull in any transitive dependencies -->
 </dependencySet>
 </dependencySets>
</assembly>

With this setup, we can invoke maven again:

robert@neghvar:~/workspace/aggregate-jar> mvn clean package                                                             
[INFO] Scanning for projects...  
(snip...)
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------------------
[INFO] Maven Recipe: Aggregate jar ........................... SUCCESS [1.562s]
[INFO] Maven Recipe: Aggregate jar - first module ............ SUCCESS [1.315s]
[INFO] Maven Recipe: Aggregate jar - second module ........... SUCCESS [0.286s]
[INFO] Maven Recipe: Aggregate jar - distribution ............ SUCCESS [1.312s]
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4 seconds
[INFO] Finished at: Sat May 15 01:26:47 EEST 2010
[INFO] Final Memory: 23M/301M
[INFO] ------------------------------------------------------------------------

And indeed a third jar has been created

robert@neghvar:~/workspace/aggregate-jar> find -name \*.jar
./aggregate-first-module/target/aggregate-first-module-1.0.0-SNAPSHOT.jar
./aggregate-second-module/target/aggregate-second-module-1.0.0-SNAPSHOT.jar
./aggregate-dist/target/aggregate-dist-1.0.0-SNAPSHOT-all-jar.jar

We verify that it contains the two classes

robert@neghvar:~/workspace/aggregate-jar> jar tf aggregate-dist/target/aggregate-dist-1.0.0-SNAPSHOT-all-jar.jar | grep \.class
ro/lmn/maven/recipe/ClassOne.class
ro/lmn/maven/recipe/ClassTwo.class

At this point, we have achieved or goal. By creating a dedicated distribution module, we have isolated the packaging logic from the rest of the project, and used the maven-assembly-plugin to package the non-transitive dependencies, which we declared to be the modules to be packaged.

One indirect conclusion of this article is that, although Maven is convention-based and seemingly rigid ( or opinionated , to meet the buzzword quota per posting ), there are often idiomatic Maven solutions which solve unsual problems in an elegant way.

The complete source code for this article is available at http://github.com/rombert/Maven-Recipe—Aggregate-Jar . If you have any suggestions or corrections, please comment. Or better yet, fork me.

Advertisements