Saturday, May 07, 2022

Keep your Maven builds DRY - leverage placeholder feature in multi-module project for version . . .

Another Maven blog post in a row, makes me feel like digging into Maven never ends ;). It's an XML world anyway, and requires considerable effort in making any small feature change to work.

I created a maven multi-module Spring Boot micro-service application a couple of years ago which is a key service for the business. Every time when there is a feature change, or a new feature addition, I always look for opportunities to upgrade tech-stack. Of course, Maven cannot be left behind. I keep upgrading maven wrapper, the tech-stack, and make build scripts better following the DRY programming principle.

Problem Context

The Spring Boot micro-service is a maven multi-module build project with about 4 sub modules (lib, domain, etl, and api). The root module and all sub-modules have semantic <version> tag specified. Due to some limitations that I ran into earlier with an older maven version, the semantic <version> tag value in all modules was repeated. So, every time when there is a version change, we had to update value in all modules. Our CI environment tags every pull request that gets merged into master/main Git branch by appending timestamp, and git-commit-id to the semantic version tag specified in the build scripts like: <semantic-version>-<timestamp in YYYYMMddHHmm format>.<short-commitId> (e.g. 2.0.1-202205070824.174cd82), thus making every commit a release candidate. However, the semantic version that we specify in maven builds is what we decide to change based on the nature of the feature. 

Environment: Java 17, Spring Boot 2.5.6, maven 3.8.5 on macOS Catalina 10.15.7

Starting from 3.5.0 maven started to allow placeholders for versions. A property (e.g. my-version) can be defined in root module with a value and the property can be used as a placeholder for <version> tags in all modules including the root module like: <version>${my-version}</version>. This feature alone might work for a single module project, but doesn't work when you have a multi-module maven build project. An extra plugin is needed for it to work. Otherwise your placeholders in sub-module dependencies are not resolved and replaced with its value and causes errors on instances like when you run any maven goal for a specific sub-module that has root module specified in <parent> block. Several blogposts and Stackoverflow question-answer references only talk about this feature with example XML snippets. 

Maven Flatten Plugin

The missing important piece is the Maven Flatten Plugin. This plugin makes the feature complete. Maven documentation about this feature does talk about this. But due to the nature of today's fast-paced development and not that great Maven's documentation, developers rely more on Stackoverflow and other direct Google hits. I also went through this, but finally ended up reading maven documentation, and test trials to make it work.

Following are examples pom.xml snippets of this feature.

Root module's pom.xml
<project ...> <groupId>com.giri.services</groupId> <artifactId>my-service</artifactId> <version>${my-service.version}</version> <modules> <module>my-lib</module> <module>my-service-domain</module> <module>my-service-etl</module> <module>my-service-api</module> </modules> <properties> <my-service.version>2.1.0-SNAPSHOT</my-service.version> ... </properties> <build> ... <plugins> ... <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>flatten-maven-plugin</artifactId> <version>1.2.7</version> <configuration> <updatePomFile>true</updatePomFile> <flattenMode>resolveCiFriendliesOnly</flattenMode> </configuration> <executions> <execution> <id>flatten</id> <phase>process-resources</phase> <goals> <goal>flatten</goal> </goals> </execution> <execution> <id>flatten.clean</id> <phase>clean</phase> <goals> <goal>clean</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>

Sub-module's pom.xml
<project ...> ... <parent> <groupId>com.giri.services</groupId> <artifactId>my-service</artifactId> <version>${my-service.version}</version> </parent> <artifactId>my-service-domain</artifactId> <packaging>jar</packaging> ... </project>

With this, next time when I want to bump up revision numbers, I only need to change the value in one pom.xml file from the root, unlike 5 earlier. That makes it DRY.

TIPS

IntelliJ inline Error in sub-module's pom.xml file
IntelliJ IDEA still complains with an error saying Properties in parent definition are prohibited for the inline placeholder in sub-module's <parent> block though you set it to use maven wrapper of your app or maven installed on your system which is higher than 3.5.0, or whatever through IDEA preferences for Maven Build.

Simply ignore this. Your build works both inside IDEA or outside from command line.

Extra . files generated
Also, notice that there are extra .flattened-pom.xml files generated in root and every sub-module folders. Just let them hang around there.

Avoid conflicting placeholder names
I wouldn't use ${revision} as  as the placeholder name for this feature as specified in the maven document when I also have the release candidate plugin. Release candidate plugin has this placeholder name reserved for git-commit-id.

References

No comments:

Post a Comment