segunda-feira, 15 de dezembro de 2014

How to Assemble Multi-Module Project with Maven

Assembling multi-module projects with Maven can be tricky. Based on different forum posts and on books, I finally could set my project to assemble for distribution.

I created prototypes to validate my ideas, using Maven 3.2.3. For learning purposes, my prototype multi-module project is like this:

Application project's structure.
  • application: it contains the parent pom file with a few and important configurations.
  •  app-distribution: created only to assemble the whole project for distribution; it has no code and no resource, only pom file configuration, and an assembly descriptor file; this approach is mention as a 'low-tech' best practice.
  • app-gui: the graphical user interface; this is the main application actually; it has resources, some included in the jar file and some to be distributed outside the jar file.
  • app-model: imaginary application model with some resources to de distributed outside the jar file.
  • app-utils: imaginary utility classes.
Besides the app-distribution, the other modules have dependencies among them and on third-party libraries, like this:

Application project's dependencies.

Before assembling the project, let's take care of the resources that must go outside of the jar file; otherwise, they will be zipped inside the jar file since they are placed in the default \src\main\resources folder. The piece of code below belongs to the pom file of the app-gui and app-model modules to exclude files with 'ini' extension:

    <build>
        <resources>
            <resource>
                <directory>${project.basedir}/src/main/resources</directory>
                <excludes>
                    <exclude>**/*.ini</exclude>
                </excludes>               
                <filtering>false</filtering>               
            </resource>            
        </resources>
    </build>

To assemble the whole project, modules with resources that go outside of the jar file must be bundled first, that is, the app-gui and app-model modules. Those modules are bundled only with their external resources; their main jar file and libs are left out at this stage.

To make that work, an assembly descriptor file must be created, and the maven-assembly-plugin must be configured to do so for each module. For this project, that is not a big deal since we have only two modules to set up, but for large projects that might be cumbersome.

So, let's think of reuse!

Firstly, even before compiling the application projects, a new project come into play, created completely independent of the application project mentioned previously; the bundle project has only an XML file which is the assembly descriptor file. It is placed in the /src/main/resources/assemblies folder (that's important), and it is named 'bundle-descriptor.xml':

    <assembly>
        <id>bundle</id>
        <formats>
            <format>zip</format>
        </formats>
        <includeBaseDirectory>false</includeBaseDirectory>
        <fileSets>
            <!-- Add external resources -->
            <fileSet>
                <directory>src/main/resources/properties</directory>
                <outputDirectory>properties</outputDirectory>
                <useDefaultExcludes>true</useDefaultExcludes>
            </fileSet>
            <fileSet>
                <directory>src/main/resources/xsd</directory>
                <outputDirectory>xsd</outputDirectory>
                <useDefaultExcludes>true</useDefaultExcludes>
            </fileSet>
        </fileSets>
    </assembly>

Execute the following command to package and install the JAR file in maven repository before working on the application project:

mvn package install

The bundle-descriptor.xml file only handles external resources; it zips them in a file to be deployed to the maven repository. The module's jar file and libs are left out.

To make use of the bundled project, the maven-assembly -plugin has to be configured the same way for both app-model and app-gui modules. Instead, only one configuration can be placed in the parent pom, within <build> and <pluginanagement> tags, to be reused later by those modules:

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <version>2.5.2</version>
                    <dependencies>
                        <dependency>
                            <groupId>blog.dacanal.assembly</groupId>
                            <artifactId>bundle</artifactId>
                            <version>1.0.0</version>
                        </dependency>
                    </dependencies>
                    <executions>
                        <execution>
                            <id>assemble</id>
                            <phase>package</phase>
                            <goals>
                                <goal>single</goal>
                            </goals>
                            <configuration>
                                <descriptorRefs>
                                    <descriptorRef>bundle-descriptor</descriptorRef>
                                </descriptorRefs>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

The bundle project is set as a dependency of the maven-assembly-plugin, and the assembly descriptor file name is set within the <descriptorRef> tags. That way, the bundle project and the configuration of the maven-assembly-plugin can be reused by those modules that make reference to the plugin in their pom file, simply like this:

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Now it's time to work on the app-distribution module. The whole application is distributed with the main jar file in the root directory, the other dependent modules and third party libs in the \lib directory, and the external resources in their own directories.

Some dependencies have to be set in the pom file of the app-distribution module. The app-gui is the main module; it becomes a dependency here, and it has the complete dependency tree of the application. The external resources of the app-gui and app-model modules are recovered by setting them as dependencies with <type> tag as 'zip' and <classifier> tag as 'bundle' (that's the id of the bundle-descriptor XML file).

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>gui</artifactId>
            <version>${project.version}</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>gui</artifactId>
            <version>${project.version}</version>
            <type>zip</type>
            <classifier>bundle</classifier>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>model</artifactId>
            <version>${project.version}</version>
            <type>zip</type>
            <classifier>bundle</classifier>
        </dependency>
    </dependencies>

The app-distribution module has its own assembly descriptor XML file with a configuration to mount our application for distribution. The descriptor file handles all dependencies (including the dependency tree) of the app-distribution module, unpacking them, or placing them on specific directories. The code of the assembly descriptor file is presented below; I added some comments to it, explaining a bit of each set of <dependencySet> tag:

    <assembly>
        <id>distribution</id>
        <formats>
            <format>zip</format>
        </formats>
        <baseDirectory>${appName}</baseDirectory>
        <dependencySets>
            <!-- Unpack all dependencies of ZIP type and BUNDLE classifier,  -->
            <!-- and unpack them in the root folder.                         -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>false</useTransitiveDependencies>
                <unpack>true</unpack>
                <includes>
                    <include>*:zip:bundle</include>
                </includes>
            </dependencySet>
            <!-- Get the main module and place it in the root dir. -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>false</useTransitiveDependencies>
                <unpack>false</unpack>
                <includes>
                    <include>*:*:jar</include>
                </includes>
            </dependencySet>      
            <!-- Get all dependency tree libs and place them in \lib folder, -->
            <!-- except those of ZIP type and BUNDLE classifier, and the     -->
            <!-- the main module. -->
            <dependencySet>
                <useProjectArtifact>false</useProjectArtifact>
                <useTransitiveDependencies>true</useTransitiveDependencies>            
                <unpack>false</unpack>
                <outputDirectory>lib</outputDirectory>
                <excludes>
                    <exclude>*:zip:bundle</exclude>
                    <exclude>${project.groupId}:gui:*</exclude>
                </excludes>
            </dependencySet>
        </dependencySets>    
    </assembly>

The configuration of the maven-assembly-plugin cannot be reused here. Actually, it has to be avoided because it is unnecessary to get the app-distribution module assembled every time the whole project is compiled and packaged (the application is not intended to be distributed on a daily basis). Then, a new configuration is created in the pom file of the app-distribution module, as described below (with comments):

    <build>
        <plugins>           
            <!-- Plugin required to assemble the project for distribution  -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.2</version>
                <!-- Not attached to any execution phase; it can only be invoked with 'mvn assembly::assembly' command. -->
                <configuration>
                    <descriptors>
                        <descriptor>src/main/assemblies/assembly-descriptor.xml</descriptor>
                    </descriptors>
                    <finalName>${appName}-${project.version}</finalName>                 
                </configuration>
                <!-- No need to run every time this module is compiled and packaged. -->
                <!-- Overwrite the parent pom configuration.                         -->
                <executions>
                    <execution>
                        <id>assemble</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <skipAssembly>true</skipAssembly>
                        </configuration>
                    </execution>
                </executions>              
            </plugin>
        </plugins>
    </build>

To get the application assembled, the user has to type the maven command in the app-distribution directory:

mvn assembly:assembly

I've been using this approach to assemble and distribute applications at work. I hope I have made myself clear and hope this post can help others to tailor their own solution using Maven.

The code I developed to validate this approach is available for download on Google Drive.

Steps To Make It Work


1) For those who are new using maven: before compiling the project modules, the parent pom file must be deployed to the local repository; run this command line from the parent directory (\application):

mvn install:install-file -DgroupId=blog.dacanal -DartifactId=application -Dversion=1.0-SNAPSHOT -Dpackaging=pom -Dfile=pom.xml

2) Package and install the JAR file of the assembly project in maven repository (\assembly):

mvn package install

3) Now, the same for the application project (\application):

mvn package install

4) Enter the app-distribution directory (\application\app-distribution) and assembly the project:

mvn assembly:assembly

The app-project will be assembled in \application\app-distribution\target\application-1.0-SNAPSHOT-distribution folder.