Maven is the most popular dependency management tool in the Java world with countless add-ons. Even if a project doesn’t use Maven for building, a lot of other build tools like Gradle, Bazel, Leiningen, or SBT use Maven repositories for keeping and obtaining libraries.
Suppose you have a project with the following structure:
pom.xml
src/
main/
java/
org/
example/
core/
Core.java
util/
FileUtil.java
cli/
Main.java
web/
Web.java
i.e. it’s some tool (e.g. static site generator named “generator”) with
- web interface ;
- command-line interface;
- some core library (the main engine);
- some filesystem utility library.
So the logical dependencies for this project look like the following:
The file utility library could be also used in other projects so it’s useful to keep it as a separate Maven artifact. Actually, the same is true for all other components. So let’s re-configure Maven to separate the components.
Step 1. Change the directories structure
Inside the main project let’s create a directory per component in the parent directory.
pom.xml
generator-web/
pom.xml
src/main/java/org/example/web/Web.java
generator-cli/
pom.xml
src/main/java/org/example/cli/Main.java
generator-core/
pom.xml
src/main/java/org/example/core/Core.java
generator-fileutil/
pom.xml
src/main/java/org/example/util/FileUtil.java
Each component’s pom.xml should be self-contained and should get dependencies via Maven. For example, generator-fileutil
pom.xml could look like:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<groupId>org.example.generator</groupId>
<artifactId>generator-fileutil</artifactId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<organization>
<name>Example Company</name>
<url>http://example.org/</url>
</organization>
<name>File Utilities Library</name>
<description>File Utilities Library</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
<distributionManagement>
<repository>
<id>target</id>
<url>http://company.example.org/maven</url>
</repository>
</distributionManagement>
</project>
By “self-contained” I mean that one can go into the corresponding directory and build the package:
cd generator-fileutilmvn clean package
The <distributionManagement>
tag specifies the URL of the company’s Maven repository to upload jar to. So the ideal organization looks like the following: whenever one pushes changes to a component, e.g. generator-fileutil
- CI (continuous integration server) discovers the changes;
- CI checks out the code and runs
mvn clean package upload
- This command uploads the new version of the JAR to the company’s Maven repository and since then it’s available for other components.
- Other components (like
generator-core
) havegenerator-fileutil
as a dependency in their pom.xml files:
<dependency> <groupId>org.example.generator</groupId> <artifactId>generator-fileutil</artifactId> <version>1.0.0-SNAPSHOT</version> <scope>compile</scope></dependency>
The maven-source-plugin
allows to upload source JAR file into the company Maven repository as well, so the IDE will recognise and attach generator-fileutil
sources when working with other components.
Now one can go inside any other component and build them with mvn clean package
regardless of the fact whether generator-fileutil
sources are located in a nearby directory or not. Thus we decouple dependency management and source management.
Step 2. The parent project pom.xml
There’s one problem remaining after Step 1. It’s difficult to change related components (e.g. generator-fileutil
and generator-core
) at the same time. Indeed to change a signature of some utility method in generator-fileutil
and its usages in generator-core
one has to
- Change and push
generator-fileutil
. - Wait until CI runs all the tests and uploads new version of
generator-fileutil
to the Maven. - Get the updated version of
generator-fileutil
from the Maven.
This is inconvenient, especially if you have to do that many times. So the encapsulating parent pom.xml is the rescue. Let’s look at our structure once again:
pom.xmlgenerator-web/generator-cli/generator-core/generator-fileutil/
This pom.xml allows to include the components as Maven modules so that the local sources of each component are used. Here’s how this pom.xml could look like:
<modelVersion>4.0.0</modelVersion>
<groupId>org.example.generator</groupId>
<artifactId>generator-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<organization>
<name>Example company</name>
<url>http://example.org/</url>
</organization>
<name>Generator App Project</name>
<description>Generator App Aggregate Project</description>
<modules>
<module>generator-fileutil</module>
<module>generator-core</module>
<module>generator-cli</module>
<module>generator-web</module>
</modules>
</project>```
That’s all. Now if one runs
mvn clean package
for that parent directory, the local sources version of each component will be used. But the main advantage is that one can open the pom.xml as a project in the IDE and all the refactorings and compilation will run on the entire codebase.