Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Maven Java Version Configuration ignored by Eclipse/Idea

I have:

<build>
  <pluginManagement>
     <plugins>
        <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.1</version>
           <configuration>
              <source>1.6</source>
              <target>1.6</target>
           </configuration>
        </plugin>
     </plugins>
  </pluginManagement>
</build>

Yet I have no problem declaring:

public enum DirectoryWatchService {

    INSTANCE;

    private java.util.Optional<String> test;
    private java.nio.file.Files files;
}

Eclipse doesn't bother. IntelliJ neither. Even Maven does not bother. I can even do a mvn clean package. Builds the darn thing without any warning whatsoever.

like image 246
John Smith Avatar asked Mar 10 '16 10:03

John Smith


1 Answers

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 ...
  • Information about APIs of earlier releases available to javac

    • Stored in a compressed fashion
    • Only provide Java SE N and JDK N-exported APIs that are platform neutral
  • Same set of release values N as for -source / -target
  • Incompatible combinations of options rejected

Main advantages of the --release N approach:

  • No user need to manage artifacts storing old API information
  • Should remove need to use tools like the Maven plugin Animal Sniffer
  • May use newer compilation idioms than the javac in older releases

    • Bug fixes
    • Speed improvements

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>
like image 125
A_Di-Matteo Avatar answered Oct 11 '22 11:10

A_Di-Matteo