22 November 2020

Summary

When Maven's Surefire plugin executes unit tests of a project, developers do not need to provide the classpath containing all dependencies. Instead, Maven sets up the required classpath. Other plugins utilize the Maven generated classpath, too. This Blog post is about some of the usages of the Maven classpath.

Maven Dependency Plugin

Have you ever wondered how the Maven setup classpath for test execution by Surefire looks like?

Let’s start with an example. Given the sample project with the following dependencies, we’ll explore the setup of the classpath generated by Maven in detail:

$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< net.gunther.cli:json2yaml >----------------------
[INFO] Building json2yaml 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ json2yaml ---
[INFO] net.gunther.cli:json2yaml:jar:0.0.1-SNAPSHOT
[INFO] +- javax.json:javax.json-api:jar:1.1:compile
[INFO] +- org.glassfish:javax.json:jar:1.1:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:5.7.0:test
[INFO]    +- org.junit.jupiter:junit-jupiter-api:jar:5.7.0:test
[INFO]    |  +- org.apiguardian:apiguardian-api:jar:1.1.0:test
[INFO]    |  +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO]    |  \- org.junit.platform:junit-platform-commons:jar:1.7.0:test
[INFO]    +- org.junit.jupiter:junit-jupiter-params:jar:5.7.0:test
[INFO]    \- org.junit.jupiter:junit-jupiter-engine:jar:5.7.0:test
[INFO]       \- org.junit.platform:junit-platform-engine:jar:1.7.0:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

The sample projects depends on the JSON-P API and its reference implementation by Glassfish. In addition, a dependency on the JUnit 5 test framework is defined. The sample project is my playground to get familiar with JSON-P functionality.

The Maven Dependency plugin provides the simplest way to display the classpath:

$ mvn dependency:build-classpath
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< net.gunther.cli:json2yaml >----------------------
[INFO] Building json2yaml 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:build-classpath (default-cli) @ json2yaml ---
[INFO] Dependencies classpath:
/home/gunther/.m2/repository/javax/json/javax.json-api/1.1/javax.json-api-1.1.jar:/home/gunther/.m2/repository/org/glassfish/javax.json/1.1/javax.json-1.1.jar:/home/gunther/.m2/repository/org/junit/jupiter/junit-jupiter/5.7.0/junit-jupiter-5.7.0.jar:/home/gunther/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.7.0/junit-jupiter-api-5.7.0.jar:/home/gunther/.m2/repository/org/apiguardian/apiguardian-api/1.1.0/apiguardian-api-1.1.0.jar:/home/gunther/.m2/repository/org/opentest4j/opentest4j/1.2.0/opentest4j-1.2.0.jar:/home/gunther/.m2/repository/org/junit/platform/junit-platform-commons/1.7.0/junit-platform-commons-1.7.0.jar:/home/gunther/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.7.0/junit-jupiter-params-5.7.0.jar:/home/gunther/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.7.0/junit-jupiter-engine-5.7.0.jar:/home/gunther/.m2/repository/org/junit/platform/junit-platform-engine/1.7.0/junit-platform-engine-1.7.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

Maven creates this classpath by considering the project’s dependencies. The reported classpath consists of references to JAR files cached in local Maven repository. Even the JAR artifact of the project is referenced from local Maven cache and not from the /target directory one or the other might expect. This means that at least Maven’s install lifecycle has to be executed before the dependency plugin is able to build the classpath correctly.

Please note, that transitive dependencies are included in the generated classpath, too.

Interestingly, the dependency:build-classpath gives the test classpath, ie. it also includes dependencies of scope test and provided. If you would like to know the classpath required for running the project’s application, the includeScope option has to be set as System property like follows:

$ mvn dependency:build-classpath -DincludeScope=compile
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< net.gunther.cli:json2yaml >----------------------
[INFO] Building json2yaml 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:build-classpath (default-cli) @ json2yaml ---
[INFO] Dependencies classpath:
/home/gunther/.m2/repository/org/glassfish/javax.json/1.1/javax.json-1.1.jar:/home/gunther/.m2/repository/javax/json/javax.json-api/1.1/javax.json-api-1.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

Maven Exec Plugin

The Maven’s exec plugin also makes a lot of use of the generated classpath. Because the exec plugin allows to execute the project’s application as defined by the POM, it’s not surprising that the required classpath is taken from Maven.

In the following, the exec goal of the plugin is used. Even if the java goal is sufficient to execute Java application, the exec is sometimes beneficial because of:

  • Allows to execute non-Java applications

  • Executes Java applications in its own JVM

The exec plugin provides another way to display the Maven generated classpath:

$ mvn exec:exec -Dexec.executable=echo -Dexec.args="%classpath"
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< net.gunther.cli:json2yaml >----------------------
[INFO] Building json2yaml 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ json2yaml ---
/home/gunther/_work/java/json2yaml/target/classes:/home/gunther/.m2/repository/javax/json/javax.json-api/1.1/javax.json-api-1.1.jar:/home/gunther/.m2/repository/org/glassfish/javax.json/1.1/javax.json-1.1.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

In this case the echo command is executed and displays the classpath passed in as %classpath placeholder. The exec plugin substitutes this placeholder when starting the application.

Please note, that the classpath does not contain test dependencies, but only reference artifacts actually required to execute the project’s application.

Another usage of the Maven generated classpath by exec plugin is to actually execute Java applications, eg.:

$ echo '{ "f1": "v1", "f2": "v2" }' | mvn exec:exec -Dexec.executable=java -Dexec.args="-cp %classpath net.gunther.cli.json.PrettyPrinter"
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< net.gunther.cli:json2yaml >----------------------
[INFO] Building json2yaml 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ json2yaml ---
{
    "f1": "v1",
    "f2": "v2"
}
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...

In this case the java executable is started, which requires the classpath to be given as -cp %classpath option. The placeholder is again substituted by references to the project’s dependencies.

The input string { "f1": "v1", "f2": "v2" } is piped to the JSON pretty-printer demo application using STDIN, the application writes given JSON in beautified format to STDOUT.

Conclusion

The classpath generated by Maven can be beneficially used in developer’s environments as demonstrated above. The dependencies of the project’s POM are considered and dependencies added to the project are automatically make their way into the generated classpath - really cool.

However, this approach is not suitable for the packaged application, because Maven is typically not available in the target runtime environment of an application. Such use cases are better implemented by packaging as single JAR that contains all dependencies. Alternatively, techniques described in my last Blog Executable Scripts with Java or approaches like jbang may be applied - the jbang project is in a very early state, but looks promising.

Tags: classpath maven maven-plugin java