Multi-module builds are generally stitched together using the
parent and modules sections of interrelated POMs.
Typically, parent POMs specify their children in a
modules
section, which under normal circumstances
causes the child POMs to be included in the build
process of the parent. Exactly how this relationship is constructed can
have important implications for the ways in which the Assembly plugin
can participate in this process, but we’ll discuss that more later. For
now, it’s enough to keep in mind this parent-module relationship as we
discuss the moduleSets
section.
Projects are stitched together into multi-module builds because
they are part of a larger system. These projects are designed to be used
together, and single module in a larger build has little practical value
on its own. In this way, the structure of the project’s build is related
to the way we expect the project (and its modules) to be used. If
consider the project from the user's perspective, it makes sense that
the ideal end goal of that build would be a single, distributable file
that the user can consume directly with minimum installation hassle.
Since Maven multi-module builds typically follow a top-down structure,
where dependency information, plugin configurations, and other
information trickles down from parent to child, it seems natural that
the task of rolling all of these modules into a single distribution file
should fall to the topmost project. This is where the
moduleSet
comes into the picture.
Module sets allow the inclusion of resources that belong to each
module in the project structure into the final assembly archive. Just
like you can select a group of files to include in an assembly using a
fileSet
and a dependencySet
, you
can include a set of files and resources using a
moduleSet
to refer to modules in a multi-module
build. They achieve this by enabling two basic types of module-specific
inclusion: file-based, and artifact-based. Before we get into the
specifics and differences between file-based and artifact-based
inclusion of module resources into an assembly, let’s talk a little
about selecting which modules to process.
By now, you should be familiar with
includes
/excludes
patterns as
they are used throughout the assembly descriptor to filter files and
dependencies. When you are referring to modules in an assembly
descriptor, you will also use the
includes
/excludes
patterns to
define rules which apply to different sets of modules. The difference
in moduleSet
includes
and
excludes
is that these rules do not allow for
wildcard patterns. (As of the 2.2-beta-2 release, this feature has not
really seen much demand, so it hasn’t been implemented.) Instead, each
include or exclude value is simply the groupId
and
artifactId
for the module, separated by a colon,
like this:
groupId:artifactId
In addition to includes
and
excludes
, the moduleSet
also
supports an additional selection tool: the
includeSubModules
flag (whose default value is
true
). The parent-child relationship in any
multi-module build structure is not strictly limited to two tiers of
projects. In fact, you can include any number of tiers, or layers, in
your build. Any project that is a module of a module of the current
project is considered a sub-module. In some cases, you may want to
deal with each individual module in the build separately (including
sub-modules). For example, this is often simplest when dealing with
artifact-based contributions from these modules. To do this, you would
simply leave the useSubModules
flag set to the
default of true
.
When you’re trying to include files from each module’s directory
structure, you may wish to process that module’s directory structure
only once. If your project directory structure mirrors that of the
parent-module relationships that are included in the
POMs, this approach would allow file patterns like
**/src/main/java to apply not only to that direct module’s project
directory, but also to the directories of its own modules as well. In
this case you don’t want to process sub-modules directly (they will be
processed as subdirectories within your own project’s modules
instead), you should set the useSubModules
flag to
false
.
Once we’ve determined how module selection should proceed for the module set in question, we’re ready to choose what to include from each module. As mentioned above, this can include files or artifacts from the module project.
Suppose you want to include the source of all modules in your
project's assembly, but you would like to exclude a particular module.
Maybe you have a project named secret-sauce
which
contains secret and sensitive code that you don't want to distribute
with your project. The simplest way to accomplish this is to use a
moduleSet
which includes each project's directory
in ${module.basedir.name}
and which excludes the
secret-sauce
module from the assembly.
Example 12.12. Includes and Excluding Modules with a
moduleSet
<assembly> ... <moduleSets> <moduleSet> <includeSubModules>false</includeSubModules> <excludes> <exclude> com.mycompany.application:secret-sauce </exclude> </excludes> <sources> <outputDirectoryMapping> ${module.basedir.name} </outputDirectoryMapping> <excludeSubModuleDirectories> false </excludeSubModuleDirectories> <fileSets> <fileSet> <directory>/</directory> <excludes> <exclude>**/target</exclude> </excludes> </fileSet> </fileSets> </sources> </moduleSet> </moduleSets> ... </assembly>
In Example 12.12, “Includes and Excluding Modules with a
moduleSet
”, since we’re
dealing with each module’s sources it’s simpler to deal only with
direct modules of the current project, handling sub-modules using
file-path wildcard patterns in the file set. We set the
includeSubModules
element to
false
so we don't have to worry about submodules
showing up in the root directory of the assembly archive. The
exclude
element will take care of excluding the
secret-sauce
module. We’re not going to include the
project sources for the secret-sauce module; they’re, well,
secret.
Normally, module sources are included in the assembly under a
subdirectory named after the module’s artifactId
.
However, since Maven allows modules that are not in directories named
after the module project’s artifactId
, it’s often
better to use the expression ${module.basedir.name}
to preserve the module directory’s actual name
(${module.basedir.name}
is the same as calling
MavenProject.getBasedir().getName()
). It is
critical to remember that modules are not required to be
subdirectories of the project that declares them. If your project has
a particularly strange directory structure, you may need to resort to
special moduleSet
declarations that include
specific project and account for your own project's
idiosyncracies.
Warning
Try to minimize your own project's idiosyncracies, while Maven is flexible, if you find yourself doing too much configuration there is likely an easier way.
Continuing through Example 12.12, “Includes and Excluding Modules with a
moduleSet
”, since we’re not processing
sub-modules explicitly in this module set, we need to make sure
sub-module directories are not excluded from the source directories we
consider for each direct module. By setting the
excludeSubModuleDirectories
flag to
false
, this allows us to apply the same file
pattern to directory structures within a sub-module of the one we’re
processing. Finally in Example 12.12, “Includes and Excluding Modules with a
moduleSet
”, we’re not interested in any
output of the build process for this module set. We exclude the
target/ directory from all modules.
It’s also worth mentioning that the sources
section supports fileSet
-like elements directly
within itself, in addition to supporting nested
fileSets
. These configuration elements are used to
provide backward compatibility to previous versions of the Assembly
plugin (versions 2.1 and under) that didn’t support multiple distinct
file sets for the same module without creating a separate module set
declaration. They are deprecated, and should not be used.
In Section 12.5.4.1, “自定义依赖输出目录”, we used the element
outputDirectoryMapping
to change the name of the
directory under which each module’s sources would be included. The
expressions contained in this element are resolved in exactly the same
way as the outputFileNameMapping
, used in
dependency sets (see the explanation of this algorithm in Section 12.5.4, “dependencySets
元素”).
In Example 12.12, “Includes and Excluding Modules with a
moduleSet
”, we used the
expression ${module.basedir.name}
. You might notice
that the root of that expression, module
, is not
listed in the mapping-resolution algorithm from the dependency sets
section; this object root is specific to configurations within
moduleSets
. It works in exactly the same way as the
${artifact.*}
references available in the
outputFileNameMapping
element, except it is applied
to the module’s MavenProject
,
Artifact
, and
ArtifactHandler
instances instead of those from
a dependency artifact.
Just as the sources
section is primarily
concerned with including a module in its source form, the
binaries
section is primarily concerned with
including the module’s build output, or its artifacts. Though this
section functions primarily as a way of specifying
dependencySets
that apply to each module in the
set, there are a few additional features unique to module artifacts
that are worth exploring: attachmentClassifier
and
includeDependencies
. In addition, the
binaries
section contains options similar to the
dependencySet
section, that relate to the handling
of the module artifact itself. These are: unpack
,
outputFileNameMapping
,
outputDirectory
, directoryMode
,
and fileMode
. Finally, module binaries can contain
a dependencySets
section, to specify how each
module’s dependencies should be included in the assembly archive.
First, let’s take a look at how the options mentioned here can be used
to manage the module’s own artifacts.
Suppose we want to include the javadoc jars for each of our modules inside our assembly. In this case, we don’t care about including the module dependencies; we just want the javadoc jar. However, since this particular jar is always going to be present as an attachment to the main project artifact, we need to specify which classifier to use to retrieve it. For simplicity, we won’t cover unpacking the module javadoc jars, since this configuration is exactly the same as what we used for dependency sets earlier in this chapter. The resulting module set might look similar to Example 12.13, “Including JavaDoc from Modules in an Assembly”.
Example 12.13. Including JavaDoc from Modules in an Assembly
<assembly> ... <moduleSets> <moduleSet> <binaries> <attachmentClassifier>javadoc</attachmentClassifier> <includeDependencies>false</includeDependencies> <outputDirectory>apidoc-jars</outputDirectory> </binaries> </moduleSet> </moduleSets> ... </assembly>
In Example 12.13, “Including JavaDoc from Modules in an Assembly”, we don’t
explicitly set the includeSubModules
flag, since
it’s true
by default. However, we definitely want
to process all modules - even sub-modules - using this module set,
since we’re not using any sort of file pattern that could match on
sub-module directory structures within. The
attachmentClassifier
grabs the attached artifact
with the javadoc classifier for each module processed. The
includeDependencies
element tells the Assembly
plugin that we're not interested in any of the module's dependencies,
just the javadoc attachment. Finally, the
outputDirectory
element tells the Assembly plugin
to put all of the javadoc jars into a directory named
apidoc-jars/
off of the assembly root
directory.
Although we’re not doing anything too complicated in this
example, it’s important to understand that the same changes to the
expression-resolution algorithm discussed for the
outputDirectoryMapping
element of the sources
section also applies here. That is, whatever was available as
${artifact.*}
inside a
dependencySet
’s
outputFileNameMapping
configuration is also
available here as ${module.*}
. The same applies for
outputFileNameMapping
when used directly within a
binaries
section.
Finally, let’s examine an example where we simply want to
process the module’s artifact and its runtime dependencies. In this
case, we want to separate the artifact set for each module into
separate directory structures, according to the module’s
artifactId
and version
. The
resulting module set is surprisingly simply, and it looks like the
listing in Example 12.14, “Including Module Artifacts and Dependencies in an
Assembly”:
Example 12.14. Including Module Artifacts and Dependencies in an Assembly
<assembly> ... <moduleSets> <moduleSet> <binaries> <outputDirectory> ${module.artifactId}-${module.version} </outputDirectory> <dependencySets> <dependencySet/> </dependencySets> </binaries> </moduleSet> </moduleSets> ... </assembly>
In Example 12.14, “Including Module Artifacts and Dependencies in an
Assembly”, we’re using the empty
dependencySet
element here, since that should
include all runtime dependencies by default, with no configuration.
With the outputDirectory
specified at the binaries
level, all dependencies should be included alongside the module’s own
artifact in the same directory, so we don’t even need to specify that
in our dependency set.
For the most part, module binaries are fairly straightforward. In both parts - the main part, concerned with handling the module artifact itself, and the dependency sets, concerned with the module’s dependencies - the configuration options are very similar to those in a dependency set. Of course, the binaries section also provides options for controlling whether dependencies are included, and which main-project artifact you want to use.
Like the sources section, the binaries section contains a couple of configuration options that are provided solely for backward compatibility, and should be considered deprecated. These include the includes and excludes sub-sections.
Finally, we close the discussion about module handling with a strong warning. There are subtle interactions between Maven’s internal design as it relates to parent-module relationships and the execution of a module-set’s binaries section. When a POM declares a parent, that parent must be resolved in some way or other before the POM in question can be built. If the parent is in the Maven repository, there is no problem. However, as of Maven 2.0.9 this can cause big problems if that parent is a higher-level POM in the same build, particularly if that parent POM expects to build an assembly using its modules’ binaries.
Maven 2.0.9 sorts projects in a multi-module build according to their dependencies, with a given project’s dependencies being built ahead of itself. The problem is the parent element is considered a dependency, which means the parent project’s build must complete before the child project is built. If part of that parent’s build process includes the creation of an assembly that uses module binaries, those binaries will not exist yet, and therefore cannot be included, causing the assembly to fail. This is a complex and subtle issue, which severely limits the usefulness of the module binaries section of the assembly descriptor. In fact, it has been filed in the bug tracker for the Assembly plugin at: http://jira.codehaus.org/browse/MASSEMBLY-97. Hopefully, future versions of Maven will find a way to restore this functionality, since the parent-first requirement may not be completely necessary.