Maven Modules
Goals
- Learn how to work with properties in a Maven project.
- Create aggregate Maven projects with multiple modules.
- Understand the distinction between Maven aggregation and inheritance.
- Managed dependencies with a parent POM.
- Package test interfaces and classes in a separate artifact.
- Filter resources with automatic property replacement.
- Known how to work with profiles.
- Configure the Maven build process using a settings file.
Concepts
- active profile
- aggregate POM
- Don't Repeat Yourself (DRY)
- effective POM
- inheritance
- monorepo
- profile
- resource filtering
- Super POM
Library
Lesson
As your applications gets more and more complex, it becomes increasingly important to organize the project modules in some meaningful way. Logically many of the modules natural separate into layers or design patterns. On a higher level the modules need to be defined in a way so that they have access to the other modules they depend on, but remain loosely coupled so that they can be understood independently and even reused. Key to this organization is the project definition, and Maven provide several facilities for separating concerns at the project level.
Properties
Before organizing modules across projects, it is important to understand how to organize the existing dependencies within a single Maven project. Maven properties are similar to constant variables. As in a computer program, these constants they allow you follow the Don't Repeat Yourself (DRY) principle. You can use Maven properties to move the definition of certain values to a common location in the POM so that they can be easily changed and then referenced later.
You already learned how to define properties when you first used Maven, setting the maven.compiler.source
and maven.compiler.target
properties. The Maven Compiler Plugin recognizes these two properties as indicating the version of Java being compiled. You can also define custom properties in this section, using the property name as the name of the XML child element. You can then reference these properties elsewhere within POM using the form ${property.name}
. Properties within Maven usually use the full-stop dot character to separate multiple words.
A common use of Maven properties is to define the dependency versions at the top of the Maven file. When a newer version of the dependency is available, the developer merely needs to update the version definition in the properties section rather than searching for the dependency declaration itself. This technique becomes even more useful with child projects, as explained under Inheritance.
Project Properties
Maven conveniently provides predefined properties, many of them representing the information defined under the root <project>
element. The names of these properties begin with a project.
prefix. One of the most common examples of project properties is project.version
, the value of which reflects the project version defined in <project><version>
. Here are a few other common and useful project properties.
project.basedir
- The base directory of the project. This is the directory containing
pom.xml
. project.groupId
- The project group ID.
project.artifactId
- The project artifact ID.
project.version
- The project version string.
project.name
- The name of the project
project.build.directory
- The directory where Maven places files generated by the build. This defaults to
target
.
Modularization
You've already been dividing your project into modules using Java packages: a package containing the business objects of application; packages representing layers of concern; and packages containing the repository pattern interface and its implementations; to name just a few examples. These modules have all been placed inside a single project.
As your program grows, it will become helpful to modularize at a higher level by separating the project itself into multiple related projects. You already did this briefly when creating data structures from scratch, organizing them in a separate datastruct
project. Just as a single project references other dependencies available via the Maven Central Repository, once you break down your project into multiple projects they can reference each other using the same dependency mechanism Maven provides. The figure shows one possible modular arrangement of projects for a car rental agency named Rent-A-Car.
Monorepos
As a matter of course each project is placed under version control in a separate Git repository. This allows each project to evolve separately, allowing for a looser coupling among dependencies. If you were to have completed your datastruct
project, for example, it would probably change infrequently; there would be no need to rebuild it each time your main program added a new feature. Moreover such a general library would likely be used by many programs, and there would be no point in placing it in any one program's repository. Each Maven project in the Rent-A-Car application might then be placed in a separate Git repository in the projects/
tree.
In some circumstances the Maven project modules are highly related. Although the modules are separated by concern, all the modules may be changing frequently. Moreover changing an interface in one module may require simultaneous updates of implementations in other modules.
In this scenario a configuration using multiple Git repositories would require multiple branches for each change. Pull requests across repositories would become cumbersome to manage and difficult to review. In such cases it is acceptable to place multiple Maven projects into a single source control repository called a monorepo. Each Maven project would typically be placed in a separate repository subdirectory, as illustrated in the figure.
Aggregation
Whether or not your Maven modules are in the same source control repository, if several of them relate to the same application you'll probably want to build them all at the same time. Maven allows you to create an aggregate POM which “aggregates” or groups several Maven projects together. Performing a Maven command on the aggregate POM will invoke the command on all the aggregated projects as well.
A POM can specify an optional <packaging>
, which defaults to jar
if this is not included. An aggregate POM explicitly indicates a <packaging>
of pom
. An aggregate POM also contains a <modules>
section; each child <module>
identifies a the directory of a Maven project to be included.
A typical location for an aggregate POM is in the directory above the modules, as shown in the figure. In this example, you could clean and package the entire Rent-A-Car project by invoking a Maven command in the directory of the aggregate POM. Maven would invoke the command on all of the defined modules in their appropriate directories.
Inheritance
Maven also provides a facility for creating a parent/child relationship between modules. The central benefit of this relationship is inheritance: as with Java subclasses, Maven modules inherit all the properties defined in a parent module. For example you could declare the default encoding <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
in the parent module, and never have to define it again in the child modules, unless you want to override it.
While aggregation is defined “downward” (that is, it is defined by the aggregate POM, not the included modules), inheritance is defined “upward”. Any module can define a parent/child relationship by indicating another module as its parent in the <parent>
section of the POM. Such a relationship is defined, not by the location of the parent, but by its coordinates—just like a dependency. The parent module must have a <packaging>
of pom
.
Dependency Management
Beyond defining properties to be used in child POMs, Maven provides an even more elegant solution for defining dependency versions at the parent module level. The <dependencyManagement>
section in a parent POM allows you to define versions and scopes of some dependencies which might be used, both in the same POM and in child POMs. This section contains a <dependency>
subsection which is formed just like the <dependency>
section you've been using. The difference is that inside <dependencyManagement><dependency>
you are merely configuring the modules as possible dependencies.
In each child POM you will still need to declare that module's dependencies. You do not however have to indicate the version, scope, or optionality of the dependencies, because the dependencies are being managed in the parent POM. You can still override any details of the dependency if you wish—by indicating a different scope explicitly, for example.
Super POM
All Maven projects use inheritance. If you do not declare a parent POM, your project will inherit from the so-called Super POM, the Maven default project that sits at the root of the inheritance hierarchy. The Super POM is what configures the default directory layout and plugin settings. The Super POM is similar to java.lang.Object
in the Java inheritance hierarchy.
When Maven builds your project, it takes the content of the project, its parent(s), and the Super POM to create a single project model. You can use the Maven Help Plugin effective-pom
goal to generate an effective POM which shows this merged model as if it were stored in a single POM.
Test JARs
A prominent stumbling block with large projects containing unit tests is how to share those tests among dependencies. When you add a dependency to a project, you get access to that dependency's code—but not its tests. A dependency's unit tests are not included the JAR Maven produces.
Imagine you are wanting building a series of related projects for a farm. Your Maven module defining the BarnyardManager
interface may use a FakeBarn
for testing storage of equipment other items in a barn. Because FakeBarn
is in the src/test/
subdirectory, Maven will not include in the resulting barnyard-1.2.3.jar
file; FakeBarn
is only used for testing. But what if the module for FarmService
would like to use FakeBarn
in its tests?
The Maven JAR Plugin provides a test-jar
goal for producing a JAR containing only the tests of a project. By default this goal is bound to the package
life cycle phase. During packaging this goal produces a separate artifact with a -tests
suffix to the base name, such as barnyard-1.2.3-tests.jar
, alongside the main artifact.
To include a test JAR as a dependency, you must indicate the coordinates of the library as normal but specify its <type>
to be test-jar
. This way Maven will know how to find the artifact with the -tests
base filename suffix.
Resources
For the vast majority of projects using the standard directory layout, Maven will look for resources in the src/main/resources/
and src/test/resources/
directories and copy them to the appropriate location in the target/
directory. This functionality is built into Maven, and is handled by the Maven Resources Plugin.
You can specify that other directories also containing resources, and they will be copied, too. Add them in the <build>
section under <resources>
, or <testResource>
for those files only related to tests. Each resource is added as a separate <resource><directory>
or <testResource><directory>
, respectively. For example you might add a section <resources><resource><directory>${project.basedir}/screenshots</directory></resource</resources>
to copy all images in the project's screenshots/
directory and include them in the generated artifact.
Filtering
By default Maven copies all resources with no changes. Maven also allows you to enable resource filtering, a useful feature that performs property replacements in the resource files when they are copied. To enable filtering, add <filtering>true</filtering>
inside any <resource>
or <resource>
definition. Most commonly instead of adding a new resource directory, you would instead redefine the existing resource directory with filtering enabled, as shown in the figure.
Once filtering is turned on, Maven will replace all properties variables in the resources with their values from the build, using the same ${property.name}
format as is used in the POM. You could use filtering to include the name and current version of your application in its readme.txt
file. The following example assumes that the project defines the project name using <name>Down on the Farm<name>
.
Profiles
One of the trademarks of a robust build system is that it is repeatable: it performs the same thing every time the build is initiated. It is essential that tests function the same on regardless of who runs the build, and that the artifact generated works the same as long as its coordinates remain unchanged. Nevertheless there is sometimes a need for variations in the build. Some developers may not want to generate extensive documentation during day-to-day development. Certain systems may need slightly different configurations based upon whether they are running Windows or Linux. Maven allows you to place build variations inside a profile, which is almost like a POM inside a POM.
Defining Profiles
Profiles are most commonly defined in the POM in a section called <profiles>
. Each <profile>
section should normally be given an <id>
so that it can be manually activated later. A profile can defines properties, plugins, and settings that do not come into effect until the profile is activated. If the profile is activate, its contents effectively are integrated into the overall POM.
In day-to-day development you may have no need to generate a separate a JAR containing your source code—after all you, already have the source code to do the build. Likewise you may not wish to generate a JAR file containing Javadoc information, as this adds time to the build. But when it comes time to distribute your product, you may wish to distribute source JARs along with it, as well as a JAR containing API documentation as a reference.
You could create a profile with the ID release
profile that would declare the Maven Source Plugin for producing a source JAR. The jar-no-fork
goal bundles all the sources as part of the same build life cycle, and is bound by default to the package
phase. This profile could also declare the Maven Javadoc Plugin using the jar
goal, which is also bound by default to the package
phase.
You can also use a profile to change the configuration of a plugin defined in the main <build>
section or that comes defined by default in Maven. Consider a typical development cycle, in which developers continually build an application on their local machines. During development it is often useful to step through the code using a debugger, which requires the use of debugging information the Java compiler places in .class
files by default. For the production build, however, you may want Maven to generate a smaller artifact with no debug information, and you may even want the compiler to provide additional optimizations. Both settings are available by configuring the Maven Compiler Plugin, which you can place in a normal <plugin>
section placed inside a profile definition.
You can just as easily define properties in a profile, which you can use elsewhere in the POM. The following example provides equivalent functionality.
Activating Profiles
The contents of a profile will not have any influence on the build unless it is an active profile. By default a profile is not active. There are several ways to make a profile active, and multiple profiles can be active at the same time.
Manual Profile Activation
You can manually activate one or more profiles explicitly on the command line when you invoke Maven. List the profiles, separated by commas, as an argument to the --active-profiles
(-P
) command-line parameter. For example you could activate both the release and production profiles shown above using -P release,production
.
Profile Activation by Default
You can also specify conditions under which a profile will be activated. These conditions appear in the <activation>
section of the profile, and the simplest condition is <activeByDefault>
. If you make a profile active by default, there is no need to manually specify a profile at all. In the following example, the development
profile will be enabled, turning on compiler debugging and disabling optimization, even if no profiles were indicated on the command line.
Profile Activation by Java Version
You can specify that a profile becomes active based upon a version of the JDK using the <jdk>
element. The profile will become activated when the JDK version starts with the given string. For example, <jdk>1.8</jdk>
would match Java builds such as 1.8.0_162
. You can indicate a range of versions, separating the lower and upper bounds using a comma; a square bracket indicates an inclusive bound, while a parenthesis indicates an exclusive bound. You can leave off one bound and use a parenthesis to indicate an unbounded range. The range <jdk>[1.8,)</jdk>
for example indicates Java 8 and above. The Disabling Javadoc Errors on Java 8 and Above section below provides a real-life example.
Profile Activation by Property
You can even activate a profile based upon which properties are set. Provide a <property>
section with both a <name>
and <value>
inside <activation>
. The following example turns on compiler debugging if the debug
property is set (such as by using -Ddebug=true
on the command line).
Other Profile Activation
There are other ways to activate profiles, including checking specific system versions and the existence of files, as shown in the following example. See Maven: The Complete Reference: 5.3. Profile Activation for more information, and Introduction to Build Profiles: How can a profile be triggered? for more examples.
Settings
Not all configuration information lives in the POM. It is crucial that passwords and other credentials are defined outside the POM, in a location that is not under source control. Although you may provide custom configuration loading from within your application, that configuration information may not be easily accessible to configure plugins in the POM itself.
Maven allows for settings to be defined outside the POM in a settings.xml
file. Settings are defined on two levels: for the user, and for the Maven installation. Each has a settings.xml
file, which you may create if it isn't already present. Global settings are located relative to the Maven installation, while each user's settings are located relative to the user's home directory.
- Global Settings
${maven.home}/conf/settings.xml
- User Settings
${user.home}/.m2/settings.xml
Servers
A primary use of settings.xml
is to declare credentials for server access. For example, if you wish to deploy your project to the Maven Central Repository, you will likely use the Nexus Staging Maven Plugin.
Permission to deploy to the Maven Central Repository is usually defined on a per-user basis, each of which has different Nexus credentials provided by Sonatype. Rather than including credentials to the Sonatype server in the POM istelf, each user who has deploy permissions will include their credentials in the user settings. The plugin configuration in the POM identifies using <serverId>
which server configuration in the settings contains the credentials.
Profiles
The settings.xml
file can be used to define new profiles independent of the the POMs themselves. A profile in the settings can define properties that are used for one or more POMs. One potential use case is a simpler mechanism than the <server>
section for providing credentials to be used in a POM, as shown in the following example. A POM could use the ${my.server.username}
and ${my.server.password}
properties in place of the actual values, requiring users to define these properties in their settings.
Profile Activation
Besides defining profiles, a settings.xml
file can specify which profiles are activated. The profiles identified in the <activeProfiles>
section will enable profiles in the settings or even those that appear in individual projects. A production server (or at least the server user with permissions to deploy the application) might indicate the release
and production
profiles in the settings.xml
file so that they need not be manually indicated on the command line.
Review
Summary
TODO summary of property sources
TODO sections that can be in a profile
Gotchas
- If you have a root
.gitignore
and you create a monorepo, you will probably need a separate.gitignore
in each project subdirectory as well or the separate tool files such as Eclipse.project
will not be ignored. - When using dependency management with child projects, you must explicitly provide the versions of the child projects rather than use properties in the parent
<dependencyManagement>
section in order for the Versions Maven Plugin to be able to update their versions correctly. - If you are filtering configuration files or other resources that already use the
${…}
delimiters for runtime interpolation, you will need to specify a different delimiter for Maven property replacement during filtering. - Don't filter binary resources such as images and sound files. Make sure Maven already ignores the appropriate file extensions when filtering; otherwise, add those to the list of non-filtered file extensions.
- Don't include credentials or other sensitive information in
pom.xml
or any other file under source control. - If you have a profile in the POM marked as active by default, it will be deactivated should you manually indicate a specific profile when invoking the build.
- If you use profiles and/or
settings.xml
, make sure you provide suitable defaults in the POM so that the main build will not break if an individual user has not set up a local configuration.
In the Real World
Filtering Properties Files Using ISO-8859-1
In earlier versions of Java, properties files were assumed to use the ISO-8859-1 charset, and resource bundles would misinterpret any non-ASCII characters if a properties file used a different charset. Java 9 now supports UTF-8 properties files, and libraries such as Rincl support UTF-8 in properties files independent of the Java version. If you need to support plain resource bundles in earlier versions of Java, you must specify a different encoding by configuring the Maven Resources Plugin.
The Maven Resources Plugin provides no direct way to specify distinct encodings for different file types; you must add a separate execution for each. Further complicating things is that by default the Maven Resources Plugin resources
goal is bound to the process-resources
phase using the execution ID default-resources
, and the testResources
goal is bound to the process-test-resources
phase using the execution ID default-testResources
. Each performs its own default copy procedure, so if you add a separate execution you will still need to modify the configuration of the default execution. See Including and excluding files and directories for more information on how to specify which resources are included.
<project …>
…
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version> <!-- use the latest version available -->
…
<executions>
<!-- ignore properties files in the default execution -->
<execution>
<id>default-resources</id>
<configuration>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering> <!-- whether non-properties files are filtered -->
<excludes>
<exclude>**/*.properties</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>filter-properties-files</id> <!-- any custom ID may be used -->
<goals>
<goal>resources</goal>
</goals>
<configuration>
<encoding>ISO-8859-1</encoding>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Disabling Javadoc Errors on Java 8 and Above
Starting with Java 8, which added a Javadoc DocLint feature for checking Javadoc format, the Maven Javadoc Plugin began considering as errors Javadoc problems that were previously considered warnings. This means that many projects would no longer build when upgrading to Java 8 if they contained Javadoc problems. The Maven Javadoc Plugin starting with version 3.0.0 provides an <additionalOptions>
configuration that allows you to specify a -Xdoclint
option for using the DocLint level to turn off DocLint, such as <additionalOptions>-Xdoclint:none<additionalOptions>
. This configuration was named <additionalparam>
before version 3.0.0; see MJAVADOC-475. However older versions of Javadoc would fail because they do not recognize the -Xdoclint
option option.
One solution was to specify a profile activated by Java version that would set the additional parameters only for versions of Java that supported it.
<project …>
…
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
…
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadocplugin</artifactId>
<version>3.0.0</version> <!-- additionalOptions named additionalparam in previous versions -->
<executions>
<execution>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<additionalOptions>${doclint.params}</additionalOptions>
</configuration>
</plugin>
</plugins>
</build>
</profile>
…
<profile>
<id>disable-doclint-java8</id>
<activation>
<jdk>[1.8,)</jdk>
</activation>
<properties>
<doclint.params>-Xdoclint:none</doclint.params>
</properties>
</profile>
</profiles>
</project>
See Maven is not working in Java 8 when Javadoc tags are incomplete and Turning off doclint in JDK 8 Javadoc for more discussion and history.
Think About It
A monorepo is not always appropriate even if projects are related. Here are some of the factors to take into consideration that would indicate a monorepo might be appropriate.
- Are the projects so closely related that they cohesively define some product or framework or platform? Normally it is possible to give some sort of name this collection of projects. The Guava project, for example, is a single library collection but is made of several modules.
- Are the projects released together with the same version? There is no requirement that all projects in a monorepo must have the same version, but synchronized versions is a factor in favor of a monorepo.
- Did the modules start out as a single project, but were split up into different modules so they could be bundled in different ways? Booker, for instance, is currently just a single program, but in the future there will be a need to deploy the command-line program separate from a server component, each using different subsets of the Booker modules.
- Is each Maven project relatively small? If you are trying to combine several large projects into the same repository, make sure that each project shouldn't continue its life independently.
Self Evaluation
- What is the difference between Maven aggregation and inheritance? Can you have a POM that uses both?
- What is the central benefit of defining a parent/child relationship between Maven modules?
- How can you disable the default execution of a Maven plugin that is already bound to a specific life cycle phase?
Task
You have been keeping your Booker application modularized with the evolution of the program. The interfaces and classes that model books and periodicals should already be in a separate model
subpackage, although you might have named the subpackage pub
or something else. You probably have I/O utility classes in a util
subpackage or similar. Your publication repository interface is likely in a repo
or repository
subpackage, and each repository implementation in some still lower-level package.
Different applications other than the Booker command-line interface program may wish to use the publication model. Applications may want to pick and choose which repository implementation to use as a dependency, without including all of them. Yet the application, model, and repository classes are highly related; it would ease development to keep them in the same repository.
Convert your booker
repository into a monorepo.
- Find those packages that could likely be used independently, such as the
model
package, and place those packages in separate subdirectories as independent projects. These subprojects will need to have appropriate dependencies added if they depend on other subprojects. - Plate the Booker command-line program itself in a separate subproject as well, such as
cli
. - Create an aggregate project in the root of your project so that all the projects can be built at the same time.
- Use the aggregate project as a parent project to the modules, so that each child project inherits property definitions and dependency management configuration.
- Use the Versions Maven Plugin to update the versions of your aggregate project and all its modules in sync.
- Place documentation files such as class diagrams in a
/doc
folder in the root of your repository.
Add a version
switch to the Booker CLI that will print out the program name and current version. Use the version of the Maven CLI subproject, but do not hard-code this value into your program. Use Maven filtering with some sort of resource or configuration file so that Maven updates the version automatically with each build. You can execute git --version
or mvn --version
to see an example of the kind of output expected of your program.
booker list [--debug] [--locale <locale>] [--name <name>] [--type (book|periodical)]
booker load-snapshot [--debug] [--locale <locale>]
booker purchase --isbn <ISBN> [--debug] [--locale <locale>]
booker subscribe --issn <ISSN> [--debug] [--locale <locale>]
booker -h | --help
booker -v | --version
Option | Alias | Description |
---|---|---|
list | Lists all available publications. | |
load-snapshot | Loads the snapshot list of publications into the current repository. | |
purchase | Removes a single copy of the book identified by ISBN from stock. | |
subscribe | Subscribes to a year's worth of issues of the periodical identified by ISSN. | |
--debug | -d | Includes debug information in the logs. |
--help | -h | Prints out a help summary of available switches. |
--isbn | Identifies a book, for example for the purchase command. | |
--issn | Identifies a periodical, for example for the subscribe command. | |
--locale | -l | Indicates the locale to use in the program, overriding the system default. The value is in language tag format. |
--name | -n | Indicates a filter by name for the list command. |
--type | -t | Indicates the type of publication to list, either book or periodical. If not present, all publications will be listed. |
--version | -v | Displays the name and current version of the Booker command-line program. |
See Also
- Maven: The Complete Reference (Sonatype)
- Monorepos in Git (Atlassian Developer)
- Introduction to the POM: Project Inheritance vs Project Aggregation (Apache Maven Project)
- Using Aggregate and Parent POMs (smartics)
- Introduction to the Dependency Mechanism: Dependency Management (Apache Maven Project)
- Specifying resource directories (Apache Maven Project)
- Filtering (Apache Maven Project)
- Including and excluding files and directories (Apache Maven Project)
- Introduction to Build Profiles (Apache Maven Project)
- Settings Reference (Apache Maven Project)
References
- Super POM
- POM 4.0.0 XSD
- Maven Model Descriptor Reference (Apache Maven Project)
- Apache Maven Filtering (Apache Maven Project)
- Version Range Specification (Apache Maven Project)
- Settings (Apache Maven Project)
- Settings 1.0.0 XSD (Apache Maven Project)