You are hitting the cross compilation misunderstanding of source
/target
options. Using a major version in your classpath (JDK 7 or 8) but wishing to compile against a minor version (6 in your case).
Compilation will be fine, but the you will have errors at runtime (i.e. NoClassDefFoundError
or NoSuchMethodError
, probably also the more generic LinkageError
).
Using source
/target
, the Java compiler can be used as a cross-compiler to produce class files runnable on JDKs implementing an earlier version of the Java SE specification.
The common belief is that using two compiler options would be enough. However, The source
option specifies against which version we are compiling while the target
option specifies the lowest Java version to support.
The compiler works on bytecode generation and source
and target
are used to generate compatible bytecode during cross-compilation. However, Java API are not handled by the compiler (they are provided as part of the JDK installation, the famous rt.jar
file). The compiler doesn’t have any knowledge of API, it just compiles against the current rt.jar
. Hence, when compiling with target=1.6
using JDK 1.7, the compiler will still point to the JDK 7 rt.jar
.
So, how can we actually have a correct cross-compilation?
Since JDK 7, javac prints a warning in case of source
/target
not in combination with the bootclasspath
option. The bootclasspath
option is the key option in these cases to point to the rt.jar
of the desired target Java version (hence, you need to have the target JDK installed in your machine). As such, javac
will effectively compile against the good Java API.
But that may still not be enough!
Not all Java API comes from the rt.jar
. Other classes are provided by the lib\ext
folder. A further javac
option is used for that, extdirs
. From official Oracle docs
If you are cross-compiling (compiling classes against bootstrap and extension classes of a different Java platform implementation), this option specifies the directories that contain the extension classes.
Hence, even using source
/target
and bootclasspath
options, we may still miss something during cross-compilation, as also explained in the official example which comes with the javac documentation.
The Java Platform JDK's javac would also by default compile against its own bootstrap classes, so we need to tell javac to compile against JDK 1.5 bootstrap classes instead. We do this with -bootclasspath and -extdirs. Failing to do this might allow compilation against a Java Platform API that would not be present on a 1.5 VM and would fail at runtime.
But.. it may still not be enough!
From Oracle official documentation
Even when the bootclasspath and -source/-target are all set appropriately for cross-compilation, compiler-internal contracts, such as how anonymous inner classes are compiled, may differ between, say, javac in JDK 1.4.2 and javac in JDK 6 running with the -target 1.4 option.
The solution suggested (from Oracle official documentation)
The most reliably way to produce class files that will work on a particular JDK and later is to compile the source files using the oldest JDK of interest. Barring that, the bootclasspath must be set for robust cross-compilation to an older JDK.
So, this is really not possible?
Spring 4 currently supports Java 6, 7 and 8. Even using Java 7 and Java 8 features and API. How can it then be compatible to Java 7 and 8?!
Spring makes use of source
/target
and bootclasspath
flexibility. Spring 4 always compiles with source
/target
to Java 6 so that bytecode can still run under JRE 6. Hence no Java 7/8 language features are used: syntax keeps Java 6 level.
But it also uses Java 7 and Java 8 API! Hence, bootclasspath
option is not used. Optional, Stream and many other Java 8 API are used. It then injects beans depending on Java 7 or Java 8 API only when JRE 7/8 are detected at runtime: smart approach!
But how does Spring assure API compatibility then?
Using the Maven Animal Sniffer plugin.
This plugin checks whether your application is API compatible with a specified Java version. Is called animal sniffer because Sun traditionally named the different versions of Java after different animals (Java 4 = Merlin (bird), Java 5 = Tiger, Java 6 = Mustang (horse), Java 7 = Dolphin, Java 8 = no animal).
You can add the following to your POM file:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.14</version>
<configuration>
<signature>
<groupId>org.codehaus.mojo.signature</groupId>
<artifactId>java16</artifactId>
<version>1.0</version>
</signature>
</configuration>
<executions>
<execution>
<id>ensure-java-1.6-class-library</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
And the build will fail as soon as you use JDK 7 API while wishing to use only JDK 6 API.
This usage is also recommended in the official source
/target
page of the maven-compiler-plugin
Note: Merely setting the target
option does not guarantee that your code actually runs on a JRE with the specified version. The pitfall is unintended usage of APIs that only exist in later JREs which would make your code fail at runtime with a linkage error. To avoid this issue, you can either configure the compiler's boot classpath to match the target JRE or use the Animal Sniffer Maven Plugin to verify your code doesn't use unintended APIs.
Java 9 Update
In Java 9 this mechanism has been radically changed to the following approach:
javac --release N ...
Will be semantically equivalent to
javac -source N -target N -bootclasspath rtN.jar ...
Main advantages of the --release N
approach:
Update on Java 9 and Maven
Since version 3.6.0
, the maven-compiler-plugin
provides support for Java 9 via its release
option:
The -release
argument for the Java compiler, supported since Java9
An example:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<release>8</release>
</configuration>
</plugin>
<pluginManagement>
and you'll see errors as high as the sky. – Isbell