Maven: 10 things you need to know

  1. The order of dependencies in pom.xml can matter because when there are multiple versions of a dependency in the dependency tree, the nearest one wins and when there are multiple nearest ones, the first declaration wins. See this SO answer. Best Practice: Don’t rely on the order of dependencies to resolve a version. Use dependencyManagement section in pom.xml to directly specify the versions of artifacts to be used when they are encountered in transitive dependencies or in dependencies where no version has been specified.
  2. Understand dependency scopes. When you read compilation, test and runtime in the official documentation, make a mental substitution with mvn compile, mvn test and mvn exec commands respectively. I found that to be helpful. There are 6 scopes defined out of which 1 is deprecated (system) and I haven’t used 2 in my work (runtime and import). That leaves compile, test and provided which are the 3 scopes you are most likely to use and they correspond to dependencies, devDependencies and peerDependencies of Node.js. In my own words:
    • Compile: Easiest to understand and works the way you would expect. This dependency will be available at compile time (i.e., when running mvn compile or mvn package) and also runtime. Note that runtime means when executing your project with mvn exec plugin. If you are executing via java command then you are responsible for declaring the full classpath and much of the discussion doesn’t apply. Also Maven will import the dependencies of a compile dependency during build and run.
    • Test: The dependency is available during compile and during testing (mvn test). However, it is not added to the classpath when you run mvn exec. This scope is intended to be used for dependencies like junit that you need while testing but are not part of your main application.
    • Provided: This is the one which is difficult to understand and causes issues like ClassNotFoundException. If you are familiar with Node.js, provided is just like peerDependencies of Node.js. The (only) use-case I am familiar with is when you are developing libraries, modules or plugins which are expected to be imported by other projects i.e., your project is not the final executable. You will never run mvn exec on your project. Having a provided dependency in your pom.xml and running mvn exec on your project is a sure way to generate a ClassNotFoundException. The project that imports your project in its pom.xml should also import your project’s provided dependencies (e.g., if A is declared as provided in your pom.xml, then the project that imports your project should import A with scope compile). Then, when mvn exec is run on that project everything will work. Any dependency declared as provided is available to you at compile time (and even test) so that your code compiles (and you can test it) but it won’t be included if you run mvn exec or in the packaged jar when you run mvn package. Note that even compile dependencies by default are not included in a jar file. We have to create a fat-jar using special plugins (Maven Shade or Maven Assembly plugin) and they will skip over provided dependencies.
    • Runtime: Say your project contains code that calls classes and methods from A. Then you definitely need A during the compile phase of your program otherwise code won’t compile. Now let’s say A calls methods from B but your project never directly calls classes or methods from B. Thus your project has an indirect dependency on B. You don’t necessarily need B to be added to the classpath when compiling your code (i.e, mvn compile). But you will need B to be available when running your program. In this case, you could add a reference to B in pom.xml and mark it with runtime scope. Or as most of us do, why bother? Maven includes a dependency with runtime scope in the runtime and test classpaths, but not the compile classpath.
    • System: Deprecated but still used. The use-case for this that I am most familiar with is to be able to reference a jar that you downloaded from somewhere (such as GitHub for example). The dependency is not available (published) on Maven Central or another repo. In that case system allows one to specify the local file path to the assembly. A better alternative is to download the file and then run mvn install:install-file (see full command here) to install it into local Maven repository and then reference it just like any other dependency with compile scope.
    • Import: I have never used this one.
  3. When things aren’t working as expected w.r.t. dependencies look at the source code. Exec plugin source code is here. And the code for core Maven which does dependency resolution is here. Look at the methods getCompileClasspathElements(), getTestClasspathElements(), getRuntimeClasspathElements() here to see the dependency scopes in action. The code is the ultimate documentation.
  4. Become familiar with mvn dependency:tree, mvn dependency:analyze, mvn dependency:list and much more. See this for full list. These will help debugging dependency issues.
  5. When there are multiple versions of a dependency in your dependency tree, only one will end up being actually used. This is what we mean by dependency resolution. Most of the time things will work but sometimes your project has reference to A and B and A must use a different version of C than B (i.e., both A and B depend on C but must use different versions of C). When this happens you will get runtime errors because only one version of C will ultimately end up in the classpath and either A or B will break. To fix this, use the Shade plugin which will basically rename all the classes in an assembly to something else. Think of it as if you had clicked on refactor and rename class in VS Code. This allows the different versions of C to co-exist in your final jar.
  6. See Maven lifecycles.
  7. To get the complete classpath (path to all the dependency jars your project uses) run mvn dependency:build-classpath. You can then use this classpath as argument to -cp flag of java command.
  8. To install a dependency from maven central to your M2 repository run (as example): mvn dependency:get -DremoteRepositories=https://mvnrepository.com/ -Dartifact=com.oracle.database.jdbc:ojdbc10:19.15.0.0.1
  9. The M2_HOME variable is deprecated and no longer used by recent Maven versions. If you want to set the path of Maven repository, the correct way to do it is to edit /path/to/maven/conf/settings.xml e.g., as:
<localRepository>/your/custom/path/.m2/repository</localRepository>

Flowchart to help in deciding Maven dependency Scope

This is just my version. I myself get very confused at times deciding the dependency scope (code for the flowchart is here).

  1. Last thing I will mention is how to execute a different main class using the maven exec plugin. The best way is to specify whatever mainClass you want to execute using the -Dexec.mainClass variable on the command-line but you can also set different main classes in pom.xml. If you want to choose this option, then you define multiple execution elements in your pom.xml. E.g.:
<execution>
                <id>some-execution-id</id>
                    <goals>
                        <goal>java</goal>
                    </goals>
                    <configuration>
                <mainClass>py4j.Py4jWrapper</mainClass> 
            </configuration>
                </execution>

and on the command-line you will use following command:

mvn exec:java@some-execution-id
  1. How do you use preview features just as Vector API with Maven? There are 2 things you need to do: first, is configuring maven compiler plugin as follows:
<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>20</source>
                    <target>20</target>
                    <compilerArgs>
                        <arg>--enable-preview</arg>
                        <arg>--add-modules</arg>
                        <arg>jdk.incubator.vector</arg>
                    </compilerArgs>
                    
                </configuration>
            </plugin>

You are not done yet. You also need to enable the same flags during execution using MAVEN_OPTS:

MAVEN_OPTS="--enable-preview --add-modules jdk.incubator.vector" \
mvn exec:java

Attempts to enable the flags using any other way will be futile as I learned painfully. It has to be done through MAVEN_OPTS.

  1. How do you run the main method in a static inner class using Maven? Use below syntax:
mvn exec:java -Dexec.mainClass='my.package.OuterClass$InnerClass'

The inner class has to be marked public or there will be an error.

  1. Sometimes you need to use --add-opens ${module}/${package} to open up the internals of a package in a Java module. You can add it to MAVEN_OPTS but as I learnt painfully while adding it to MAVEN_OPTS works for mvn exec:java, it does NOT work for mvn test [1]. mvn test ignores any --add-opens directives specified in MAVEN_OPTS. To get around the limitation, run your tests that require --add-opens using the java command like below (shown using JUnit4 test runner as example):
java \
-cp $CLASSPATH \
--add-opens java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens java.base/java.nio=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
org.junit.runner.JUnitCore \
com.mycompany.app.BdbTest

If you like watching a video, I recommend this.

  1. While using Maven, take care not to set any properties/flags in the <configuration> section of a plugin e.g., see this. The reason is that if you do this, you will not be able to override the settings from command-line. Instead of having the command-line override pom.xml, Maven overrides the command-line with pom.xml. How infinitely stupid!
  2. If you use mvn exec:java to run a program, understand how it operates as it introduces some nuances and may create unexpected warnings due to the fact that your program is not executed on the main thread. e.g., see this.

Basics

Maven has concept of lifecycles -> phases -> goals. Plugins bind to phases. To execute a phase Maven delegates the execution to a plugin. When you run a Maven command e.g. when you run:

mvn install

You are executing the install phase. Running install will also run all the previous phases in the lifecycle its associated with.

<phases>
  <phase>validate</phase>
  <phase>initialize</phase>
  <phase>generate-sources</phase>
  <phase>process-sources</phase>
  <phase>generate-resources</phase>
  <phase>process-resources</phase>
  <phase>compile</phase>
  <phase>process-classes</phase>
  <phase>generate-test-sources</phase>
  <phase>process-test-sources</phase>
  <phase>generate-test-resources</phase>
  <phase>process-test-resources</phase>
  <phase>test-compile</phase>
  <phase>process-test-classes</phase>
  <phase>test</phase>
  <phase>prepare-package</phase>
  <phase>package</phase>
  <phase>pre-integration-test</phase>
  <phase>integration-test</phase>
  <phase>post-integration-test</phase>
  <phase>verify</phase>
  <phase>install</phase>
  <phase>deploy</phase>
</phases>

Here you can see what plugin Maven executes to execute a phase for the jar packaging:

<phases>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:2.6:resources
  </process-resources>
  <compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
  </compile>
  <process-test-resources>
    org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
  </process-test-resources>
  <test-compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
  </test-compile>
  <test>
    org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
  </test>
  <package>
    org.apache.maven.plugins:maven-jar-plugin:2.4:jar
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:2.4:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
  </deploy>
</phases>

As I understand mvn command has two forms:

mvn phase
mvn plugin:goal

e.g. when we run mvn compile we are invoking the well-defined compile phase and this phase maps to compile goal of org.apache.maven.plugins:maven-compiler-plugin by default. In contrast, when we run

mvn spring-boot:run

we are telling directly the goal and plugin to execute.

Each goal is a MOJO – Maven Old Java Object. To debug with Maven use mvndebug (alternatively use jdb to debug directly without Maven).

How to override the default plugin associated with a phase?

When we declare a plugin in pom.xml under:

<build>
  <plugins>
    <plugin>

we are essentially overriding the default plugin associated with a phase. You can use this to e.g., use the shade plugin to package your jar instead of the default maven-jar-plugin.

Homework

Think you know Maven? How about filling this table with a ✔ or ×

scopemvn compilemvn testmvn execmvn packagemvn test-compilemvn dependency:build-classpath
compile
test
runtime
provided

re: mvn package, the question is to tell whether a dependency will be included in the fat jar or not when running mvn package with the shade plugin. I don’t recommend using assembly plugin and for spring boot you should use spring-boot-maven-plugin for building a fat jar.

Other Tips

Today I learned this command from ChatGPT:

tr '\0' ' ' < /proc/<PID>/cmdline

Its an incredibly useful command as it shows the command that launched a Linux process. E.g., when you run a spring boot application using mvn spring-boot:run, above command can reveal the command that Maven runs behind-the-scenes that actually spins up the java process. It looks like this e.g.:

/home/ubuntu/jdk-21.0.6/bin/java -classpath /home/ubuntu/apache-maven-3.9.9/boot/plexus-classworlds-2.8.0.jar -Dclassworlds.conf=/home/ubuntu/apache-maven-3.9.9/bin/m2.conf -Dmaven.home=/home/ubuntu/apache-maven-3.9.9 -Dlibrary.jansi.path=/home/ubuntu/apache-maven-3.9.9/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/ubuntu/project-dir org.codehaus.plexus.classworlds.launcher.Launcher spring-boot:run -Dspring-boot.run.jvmArguments=-Djava.library.path=lib -DJWT_SECRET_KEY= -Dspring-boot.run.main-class=my.web.WebApplication -Dspring-boot.run.arguments=--server.port=8080 

It looks like there are 2 java processes that are spawned. One is above and there is yet another java process that is like below:

/home/ubuntu/jdk-21.0.6/bin/java -XX:TieredStopAtLevel=1 -Djava.library.path=lib -DJWT_SECRET_KEY= -cp <full_class_path> my.web.WebApplication --server.port=8080

This is incredibly useful as it gives you the full classpath you need if you later want to launch without Maven. Many times I have to use Maven just to launch an application as constructing the humunguous classpath is impossible manually.

This entry was posted in Computers, programming, Software and tagged . Bookmark the permalink.

Leave a comment