Building modularized Java application targeting both Java 9 and older - java

My application is working fine with Java 8 and older but fail to load classes that are not part of Java SE and fails with:
class MyClass (in unnamed module #0x4d14b6c2) cannot access class
sun.tools.attach.HotSpotVirtualMachine (in module jdk.attach) because
module jdk.attach does not export sun.tools.attach to unnamed module
#0x4d14b6c2.
My original idea was to modularize my application and figure out if the classes I need have a public module to declare the dependency properly. However, as soon as I have declared dummy module-info.java, javac started to complain:
modules are not supported in -source 1.6; use -source 9 or higher to enable modules
EDIT: I am instructing maven-compiler-plugin to produce java 6 compatible bytecode as I need the application to support it. The actual arguments used are -g -nowarn -target 1.6 -source 1.6 -encoding UTF-8.
Perhaps I am missing something obvious but how do I build a jar that would work with Jigsaw as well as older java versions without it?

With help from Jigsaw development mailing list, I managed to get the modularized codebase to build. (I ended up not needing the HotSpotVirtualMachine in particular).
Compile module-info.class with -source 9 -target 9 for the app to be runnable on older JVMs as those have no reason to load the class.
The rest of the classes should be compiled with appropriate source/target level (Java 9 does not change anything here).
So two javac invocations are needed: by default only classes are compiled and module descriptors are skipped, if compiled using Java 9, module descriptors are compiled skipping all the regular classes.
Maven pom.xml declaration to keep the application buildable and runnable across different JDK versions is a bit clumsy but working:
<!--
When using compiler from java 8 and older, ignore module files altogether.
Otherwise, use 2 phase compilation to build
- all classes for target version and
- module-info.java with 9+ source and target level
-->
<profiles>
<profile>
<id>jigsaw</id>
<activation>
<jdk>[1.9,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.9</source>
<target>1.9</target>
</configuration>
<executions>
<execution>
<id>module-infos</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<includes>
<include>**/module-info.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<excludes>
<exclude>**/module-info.java</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Related

Force maven to run tests in a Spring Boot app with a specific jdk

I'm running Ubuntu 22.04 right now, and trying to use maven toolchains to compile a Java 17 Spring Boot project while my active jdk is JDK11. I set up my toolchains so they use JDK18 to compile it, and this works great. However, when I try to actually run the tests (with mvn clean install), I get the following error:
...ApplicationTests has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0
Everything works out fine when I run mvn clean install -DskipTests. I can see the testCompile goal executing and the test classes being compiled. I'm guessing the issue is that, after compilation with JDK18, maven still tries to use JDK11 to actually run the tests. Any idea how I can tell maven to run the tests with the correct JDK, when I do mvn clean install or similar?
Edit: I managed to get it to work by hardcoding my JDK18 path for the maven surefire plugin. It looks like this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<jvm>/usr/lib/jvm/java-18-openjdk-amd64/bin/java</jvm>
</configuration>
</plugin>
While this is alright, I don't like the hardcoding. According to the documentation (seems out of date, but eh), maven-surefire-plugin should be toolchain aware, and should be able to work if I specify my toolchain with something like
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<jdkToolchain>
<version>18</version>
<vendor>sun</vendor>
</jdkToolchain>
</configuration>
</plugin>
or not even that, since surefire should be able to derive this from the toolchain I configure with the maven-toolchains-plugin. Relevant toolchain config, just for the record:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>18</version>
<vendor>sun</vendor>
</jdk>
</toolchains>
</configuration>
</plugin>

What is the difference between maven compiler plugin and maven toolchains plugin?

I had to integrate some legacy code into my maven build, so I used the maven-recommended toolchains plugin to change the java version:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>1.5</version>
</jdk>
</toolchains>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.0</version>
<configuration>
<compilerArgs>
<arg>-Xmaxerrs</arg>
<arg>1000</arg>
</compilerArgs>
</configuration>
</plugin>
Then I ran into the max 100 compile errors problem which required passing special options to javac and found I was able to do it just with maven compiler:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.0</version>
<configuration>
<fork>true</fork>
<compilerVersion>1.5</compilerVersion>
<executable>C:\Java\jdk-1.5.0_22\bin\javac.exe</executable>
<source>1.5</source>
<target>1.5</target>
<compilerArgs>
<arg>-Xmaxerrs</arg>
<arg>1000</arg>
</compilerArgs>
</configuration>
</plugin>
Both snippets produce the same result: the java compiler is changed from the maven default to java 1.5. Both run in about the same amount of time, so there's no visible performance difference. I'd like to know if there are any benefits of one over the other so I know when to use each.
They do different things:
The compiler plugin specifically configures how your Java code is compiled (and only that).
The toolchains plugin just ensures that other plugins are all using the same Java tool chain (i.e. the same JDK) to compile, run, test, generate javadocs and so on.
This is explained in the respective plugins' documentation.
Note that not all plugins are "tool chain aware", but the compiler plugin is.
... are any benefits of one over the other
Well there there are things you can do with one and not the other and vice versa. For example, you can't set Java compiler options using the toolchain plugin.
However, they are not mutually exclusive. You can use both in the same POM file.

Maven :: Multi-Module project built by different JDK per module [duplicate]

My maven project has a few modules: server, web, etc.
I would like to build all but my server module on Java 6. For the server module, I'd like to compile it with Java 7.
Here's my pom.xml below, but I think that if I modify it to 1.7, then all of my modules will be compiled with Java 7. Also, does maven use the JAVA_HOME environment variable to determine which Java version to use?
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<memmax>2048m</memmax>
</configuration>
</plugin>
EDIT Also, does the below output of
maven --version
indicate that maven is compiling my java code with 1.7?
vagrant#dev:~/bin/apache-tomcat-7.0.29/bin$ mvn --version
Apache Maven 3.0.4 (r1232337; 2012-01-17 08:44:56+0000)
Maven home: /home/vagrant/bin/apache-maven-3.0.4
Java version: 1.7.0_07, vendor: Oracle Corporation
Java home: /home/vagrant/bin/jdk1.7.0_07/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.2.0-23-generic", arch: "amd64", family: "unix"
Thanks,
Kevin
There are a number of hacks out there for compiling source code with a different version of the JDK than you are using to run Maven, for example you can use something like
<project>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<executable><!-- path-to-javac --></executable>
</configuration>
</plugin>
</plugins>
[...]
</build>
[...]
</project>
The issue with this approach is that you now have hard-coded the path to the JDK into your POM. Everything will work just fine on your machine but when you have to rebuild your machine because the HDD failed, or when you want to build on a different machine, you will be stuck as the path will most likely not match up.
The correct best practice way to handle this is via Toolchains. This will see you creating a ~/.m2/toolchains.xml file that describes where each of the different toolchains in your system are. Then the version of the JDK can be applied by the Maven Toolchains Plugin, e.g.
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>1.6</version>
</jdk>
</toolchains>
</configuration>
</plugin>
...
</plugins>
The next thing is that you don't need this as often as you would think. For example by using the source and target values you can generate the correct bytecode for the JRE that you are targeting... the only issue that you will then hit is the use of methods that are new in JRE 1.7... which is where Mojo's Animal Sniffer Plugin comes in. Animal Sniffer can be used to ensure that you only use the methods of the JRE that you are targeting. The general community consensus is that the use of source and target configuration options in the Maven Compiler Plugin configuration coupled with the use of Mojo's Animal Sniffer virtually eliminates the need for toolchains on the Compiler end of things.... on the Surefire end of things there is still need for toolchains... and I have a few edge cases that I need to update the compiler plugin and the toolchains plugins for to handle but, realistically you will not hit those edge cases ;-)
Just to be sure that your original question is completely answered (since the above answers the question you wanted to ask - as opposed to the one you asked)
At present you are compiling with JDK 1.7 however depending on the version of the Maven Compiler Plugin you are using, you may be compiling with either <source>1.4</source><target>1.4</target> or <source>1.5</source><target>1.5</target> unless you have changed the configuration of the Maven Compiler Plugin in your pom.xml. That will dictate which language features are available to you, but not which classes... so you would be generating code that will work on JRE 1.7 and provided you have not used any new classes/methods introduced since 1.4/1.5 (Such as String.isEmpty()) should also work on JRE 1.4/1.5... the only way to be sure if it works on such an old JVM is to either: run it on the old JVM OR use Animal Sniffer.
Maven Toolchains
To use multiple Java versions, you need to use Maven Toolchains, which require you to create a toolchains.xml file in your ~/.m2 Maven folder, containing all Java versions installed on your machine:
<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<id>Java13</id>
<version>13</version>
</provides>
<configuration>
<jdkHome>${env.JAVA_HOME_13}</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<id>Java9</id>
<version>9</version>
</provides>
<configuration>
<jdkHome>${env.JAVA_HOME_9}</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<id>Java8</id>
<version>8</version>
</provides>
<configuration>
<jdkHome>${env.JAVA_HOME_8}</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<id>Java7</id>
<version>7</version>
</provides>
<configuration>
<jdkHome>${env.JAVA_HOME_7}</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<id>Java6</id>
<version>6</version>
</provides>
<configuration>
<jdkHome>${env.JAVA_HOME_6}</jdkHome>
</configuration>
</toolchain>
</toolchains>
The JAVA_HOME_13, JAVA_HOME_9, JAVA_HOME_8, JAVA_HOME_7, JAVA_HOME_6 environment variables are configured so that they reference the path where the associated Java version is installed.
The FlexyPool parent pom.xml configuration file
The parent pom.xml Maven configuration file of the FlexyPool project defines the global Java version settings
<properties>
<jdk.version>8</jdk.version>
...
</properties>
Now, we need to instruct both the compiler and the test plugins to use the configured java version.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>${jdk.version}</version>
</jdk>
</toolchains>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
<showDeprecation>true</showDeprecation>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</build>
The FlexyPool child Maven module pom.xml using a different Java version
The flexy-pool-core-java9 child Maven module that requires a different Java version only needs to override the default jdk.version Maven property:
<properties>
<jdk.version>9</jdk.version>
</properties>
And that's it, we can now build each module using its own minimum viable Java version.
use the setup for the JDK6 on your top pom, it will be inherited by all the module, and overwrite it for your server pom with the different configuration required.
As for the path of the JDK, you can specify it, see here: http://maven.apache.org/plugins/maven-compiler-plugin/examples/compile-using-different-jdk.html

Maven compile aspectj using different jvm

I have a project using SwingUtilities2 which is available in Java 5 only. I'm trying to compile it using newer JDK (using Jenkins). When it comes to pure Java only I have no problems, I simply set the compile plugin like this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<compilerVersion>${jdk.version}</compilerVersion>
<executable>${jdk.path}/bin/javac</executable>
<fork>true</fork>
</configuration>
</plugin>
Where ${jdk.path} points to Java 1.5 JDK.
When trying to add aspects, it always compiles using default jdk (1.8).
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<complianceLevel>${jdk.version}</complianceLevel>
</configuration>
</plugin>
I was trying as well setting compilerId + dependencies in maven-compiler-plugin (see: http://maven.apache.org/plugins/maven-compiler-plugin/non-javac-compilers.html), but didn't help either.
Any clues how to do it?
EDIT:
Changed Jenkins job type from Maven to Free Style and invoke it using JDK 1.5. Now compiles with aspects.

Maven: Build code against Java 1.4, but build test code against Java 1.5

In Maven, I have a codebase that I want to build that needs to target the 1.4 JVM. This is very easy to do in the pom, but I have one problem: The tests for this codebase use 1.5+ constructs.
Is it possible to have Maven compile/run the tests inside a 1.6 JVM, but build the main codebase to target 1.4?
Setting source to 1.6 and target to 1.4 don't work. Maven/Java don't allow this combination.
This is possible, but you need to set the parameters for testCompile rather than compile. You can specify a different target/source combination for the testCompile that you use for the compile.
So for compile you've have a target of 1.4, and testCompile 1.5 or 1.6.
Also, to run the unit tests, you can specify the jvm to use in surefire by using the jvm parameter. This would point to a 1.6 jvm.
It's been awhile since I've done this, but the plugin dependency you want is something like this:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<source>1.3</source>
<target>1.3</target>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</execution>
</executions>
</plugin>
link to source
I was able to achieve what I wanted with this:
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<testSource>1.6</testSource>
<testTarget>1.6</testTarget>
<target>1.4</target>
<source>1.4</source>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

Categories

Resources