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.

About these ads

10 Comments »

  1. sani said

    Hi ,

    Very interesting article, This is exactly what I was looking for. I tried to acheive the same thing but Im getting the following error. Any idea?

    [INFO] ————————————————————————
    [INFO] Building Lecture A Distance – Axe 1 – Distribution
    [INFO] task-segment: [clean, package]
    [INFO] ————————————————————————
    [INFO] [clean:clean {execution: default-clean}]
    [INFO] Deleting directory D:\LAD\CODE\LAD-DESIGN\lad-ed-distribution\target
    [INFO] [site:attach-descriptor {execution: default-attach-descriptor}]
    [INFO] [assembly:single {execution: package-all}]
    [INFO] Reading assembly descriptor: src/main/assembly/all-jar.xml
    [WARNING] Cannot include project artifact: ca.qc.hydro.ed:lad-ed-distribution:po
    m:1.0.0; it doesn’t have an associated file or directory.
    [INFO] ————————————————————————
    [ERROR] BUILD ERROR
    [INFO] ————————————————————————
    [INFO] Failed to create assembly: Error creating assembly archive all-jar: You m
    ust set at least one file.

    [INFO] ————————————————————————
    [INFO] For more information, run Maven with the -e switch
    [INFO] ————————————————————————
    [INFO] Total time: 6 seconds
    [INFO] Finished at: Fri Aug 06 09:38:08 EDT 2010
    [INFO] Final Memory: 25M/247M
    [INFO] —————

    Thanks,

    Sani

    • Robert said

      Hi Sani,

      I suggest you start from a working example – my Github repo is one – and expand on it. It’s sometimes simpler than debugging an existing problem.

      Cheers,

      Robert

  2. Andrew said

    Thanks for article Robert, it helped me to solve problem with dependencies.

    • Robert said

      You’re welcome, Andrew.

  3. Mariusz said

    Hi!

    How to do the same with gwt-maven-plugin?
    I want to have client module, server module and result of compilation as war placed in dist module??

    Help!

  4. Vick said

    Thanks for documenting this technique. Works great. I used your example code checked into github (as linked above)

    • Robert said

      You’re welcome, glad to be able to help.

  5. Torbjörn said

    It’s a pity I didn’t google my way here before head-butting into a similar solution. I’m having one problem with this one, and I wonder if anyone has any ideas.

    Adding a dependency to the dist jar works, however this will result in pulling in duplicate code (both the aggregated jar and all of its dependencies). I tried setting the dependencies in the dist project as scope provided which kinda works. However, now I don’t get the transitive dependencies. Is there a way to “publish” the transitive dependencies (without including them in the dist jar)?

    Thanks

  6. Robert Yeates said

    Magic, thank you for this! Maven is so powerful but it’s lacklustre documentation is very fustrating

RSS feed for comments on this post · TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: