Monday, January 20, 2020

The Forgotten Maven . . .

It's been more than a decade since I used Maven, or Maven used me. I was quite impressed with it's dependency management and convention over configuration features first time when I tried and introduced it in a project. But it was not a long-lasting impression. Those were the days when Ant was the de-facto standard build system for Java projects, XML/XSD/XSLT were pleasing every developer, and there wasn't any build system with dependency management capability.

Then I moved to a team where Ant + Ivy was chosen standard in Maven era. Huh...what a pain to go back to Ant and start writing build scripts in XML from scratch with Ivy as the dependency manager and your own conventions & configurations for a project! Later, I was privileged at the same place to revamp their tech-stack for one of the new projects, changing from AccuRev to Git, Java to Java + GroovyStruts to Spring MVC, WebLogic to Tomcat, no CI/CD to full Jenkins CI/CD pipeline, and Ant + Ivy to Gradle. The last one: Ant + Ivy to Gradle, was a joyful great leap forward. I was very impressed with the depth of Gradle documentation. I started advocating by popularizing the same phrase borrowed from Gradle docs, "Next Generation Build System". Then I chose and moved onto Grails projects. For about 5 years, I was living in a very happy world of Groovy, Grails and Gradle. My vision got better with no visual noise and clutter ;)

Back to the Future

Now I am back to Java, Spring Boot tech space where Maven is the chosen standard build system. Whenever I open pom.xml file in IDE, my eyes suddenly get blurry and my fingers start to slide on the trackpad making the screen scroll up and down. "Welcome back!", I say to myself as this is my choice to move back to Java ;)

Lately, I was going through the steps for building and running an existing multi-module project (multi-project in Gradle). A step describing to install specific version of maven paused me. Also, to run maven-goals (equivalent to gradle-tasks) of a specific module (project in Gradle) in a multi-module (multi-project in Gradle) proejct, I had to cd into it to run it's goals. My immediate reaction was to google and explore these two features: 1) Multi-module builds 2) Maven wrapper. Going forward, I would apply these two to every maven build-based project.

Environment: Java 13, Spring Boot 2.2.3, Maven 3.6.2 on MacOS High Sierra 10.13.6

1. Maven Wrapper (similar to Gradle wrapper)

Working with Gradle based Grails projects, I am used to Gradle wrapper which is preferred way to go without having Gradle or a specific version of Gradle installed to build and run your project. I was happy to find something similar exists now for Maven world. If one exists, why not use it? I started using it. Conceptually, it is very similar to Gradle wrapper.

2. Multi-module maven build (similar to multi-project gradle build)

One of the pain points I had with a multi-module project was finding how to run maven goals in the context of a specific module (project) from the root project directory. But finding a way to do this took little more time than expected even when we have stackoverflow around to readily help finding a way. If a maven expert reads this and says, "Hey stupid, this is so obvious and maven users already know how to do this even in sleep.", I am not ashamed to take it with a smile ;)

Using Maven Wrapper in a Multi-module maven Project

In a multi-module maven project, apply maven wrapper at the root project level. It generates a couple of command scripts (mvnw, mvnw.cmd) and .mvn/wrapper dir used by the wrapper scripts to go fetch, install and run maven if it is not present on your system.

With the following multi-project structure (my-service is the root project, my-service-api is a Spring Boot project):

. └── my-service ├── my-service-api │   └── pom.xml ├── my-service-lib │   └── pom.xml └── pom.xml


Run the following wrapper plugin goal from the root project dir: my-service (of course, for running this, you need to have maven installed) to add maven wrapper support to your multi-module project:
mvn -N io.takari:maven:wrapper

The above command generates wrapper specific files and directory that are highlighted and shown below:
. └── my-service ├── .mvn │   └── wrapper │   ├── MavenWrapperDownloader.java │   ├── maven-wrapper.jar │   └── maven-wrapper.properties ├── mvnw ├── mvnw.cmd ├── my-service-api │   └── pom.xml ├── my-service-lib │   └── pom.xml └── pom.xml

All files added by the wrapper plugin should also be checked-in and live in the repo along with project files. Now, if a new developers check out the project, they don't need to install maven to build and run the project on their systems. Simply run maven goals from the root project like, but instead of running mvn command which requires maven to exists, happily run ./mvnw wrapper script.

Maven multi-project builds can be run from either the root project or from a specific module. When run from the root, it builds all sub-modules. When run from a specific module, it just builds that module. The root level pom.xml defines modules and other common configurations available for all modules. A module specific pom.xml defines build configuration for that specific module.

Without wrapper support and maven installed, or with wrapper support and maven not installed, one can run multi-project builds in 3 ways:
    1. Build from the root, which builds all modules (sub-projects).
    2. Build from the root, but a specific module's specific goal.
    3. Build from a specific module, cd into a specific module and run that module's specific goal.

1. Build from the root project

// from the root project $ cd my-service // with maven (installed) my-service$ mvn clean install // with maven wrapper (maven not installed) my-service$ ./mvnw clean install

2. Build a specific module from the root project

// from the root project $ cd my-service // with maven (installed) my-service$ mvn -pl my-service-api clean install spring-boot:run -Dspring-boot.run.profiles=local
// with maven wrapper (maven not installed) my-service$ ./mvnw -pl my-service-api clean install spring-boot:run -Dspring-boot.run.profiles=local

-pl <sub-module-name-the-tasks-to-be-run-for> is the key command option to know for this.

3. Build specific module's goal from the module

// from the module $ cd my-service-api // with maven (installed) my-service-api$ mvn -pl clean install spring-boot:run -Dspring-boot.run.profiles=local
// with maven wrapper (maven not installed) my-service-api$ ../mvnw clean install spring-boot:run -Dspring-boot.run.profiles=local

TIP

mvn -h or mvn --help lists all command line options.

Here is how the help looks for -pl option:
-pl,--projects <arg> Comma-delimited list of specified reactor projects to build instead of all projects. A project can be specified by [groupId]:artifactId or by its relative path

I got fooled and stayed away by the buzz word reactor projects when I first tried looking for some help before seeking further help and finding how-to on stackoverflow :(

1. Newer versions of Maven - Wrapper support

With newer versions of Maven, adding wrapper to maven project is easy, just run the following command from the project root folder:
mvn wrapper:wrapper // add wrapper support to project ./mvnw -v // check maven version used by wrapper ./mvnw validate // validate project

To upgrade maven wrapper to newer version of maven, just run the following command and checkin all modified files into your source repo:
./mvnw wrapper:wrapper -Dmaven=3.8.6 // upgrade maven wrapper to newer version of maven ./mvnw -v // check maven version used by wrapper

2. Show Maven Version when maven commands are run using Maven wrapper

Add file under .mvn directory of your project named: maven.config containing --show-version and you will have both Maven version and Maven home directory displayed in the build output in the beginning.

3. Show Java Version when maven commands are run using Maven wrapper

Add file under .mvn directory of your project named: jvm.config containing --show-version and you will have Java version displayed in the build output in the beginning.

Also, the following config entries in the above mentioned jvm.config file will be useful to show date-time of build console log lines and thread name as well.

-Dorg.slf4j.simpleLogger.showDateTime=true -Dorg.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss -Dorg.slf4j.simpleLogger.showThreadName=true --show-version

Summary

After happily living in Groovy/Gradle/Grails world for about a decade, whether I like it or not, I am back to Java/Maven and I am using maven again. Oops, maven started using me again ;)

References



No comments:

Post a Comment