Maven Java 版本配置被 Eclipse/Idea 忽略

Maven Java Version Configuration ignored by Eclipse/Idea

我有:

<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>

然而我声明没有问题:

public enum DirectoryWatchService {

    INSTANCE;

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

Eclipse 不会打扰。 IntelliJ 也没有。甚至 Maven 也不会打扰。 我什至可以做一个 mvn clean package。在没有任何警告的情况下构建该死的东西。

你遇到了source/target选项的交叉编译错误。在您的 class 路径(JDK 7 或 8)中使用主要版本,但希望针对次要版本(在您的情况下为 6)进行编译。
编译没问题,但是你会在 运行time 时出错(即 NoClassDefFoundError or NoSuchMethodError, probably also the more generic LinkageError)。

使用 source/target,Java 编译器可以用作交叉编译器来生成 class 文件 运行nable on JDKs implementing Java SE 规范的早期版本。

普遍认为使用两个编译器选项就足够了。但是,source 选项指定我们正在编译的版本,而 target 选项指定要支持的最低 Java 版本。

编译器用于生成字节码,sourcetarget用于在交叉编译时生成兼容的字节码。然而,Java API 不被编译器处理(它们作为 JDK 安装的一部分提供,著名的 rt.jar 文件)。编译器不知道 API,它只是针对当前的 rt.jar 进行编译。因此,当使用 JDK 1.7 编译 target=1.6 时,编译器仍将指向 JDK 7 rt.jar.

那么,怎样才能真正正确的交叉编译呢?

Since JDK 7, javac prints a warningsource/target 未与 bootclasspath 选项组合的情况下。在这些情况下,bootclasspath 选项是指向所需目标 Java 版本的 rt.jar 的关键选项(因此,您需要在您的计算机中安装目标 JDK机器)。因此,javac 将有效地针对好的 Java API.

进行编译

但这可能还不够!

并非所有 Java API 都来自 rt.jar。其他 classes 由 lib\ext 文件夹提供。为此使用了另一个 javac 选项,extdirs。来自官方 Oracle 文档

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.

因此,即使使用 source/targetbootclasspath 选项,我们仍然可能在交叉编译期间遗漏一些东西,正如 official example 中所解释的那样使用 javac 文档。

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.

但是..可能还是不够!

来自Oracle官方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.

建议的解决方案(来自 Oracle official 文档)

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.

所以,这真的不可能吗?

Spring 4 currently supports Java 6, 7 and 8。即使使用 Java 7 和 Java 8 个特征和 API。那么它如何与 Java 7 和 8 兼容?!

Spring 利用了 source/targetbootclasspath 的灵​​活性。 Spring 4 始终使用 source/target 编译为 Java 6,以便字节码在 JRE 6 下仍然可以 运行。因此没有 Java 7/使用了8种语言特性:语法保持Java 6级。
但是它也用了Java7和Java8API!因此,不使用 bootclasspath 选项。可选,Stream 和许多其他 Java 8 API 被使用。然后它根据 Java 7 或 Java 8 API 仅在 运行 时间检测到 JRE 7/8 时注入 bean:聪明的方法!

但是Spring如何保证API兼容性呢?

使用 Maven Animal Sniffer 插件。
此插件检查您的应用程序是否 API 与指定的 Java 版本兼容。之所以称为动物嗅探器,是因为 Sun 传统上以 different animals 命名不同版本的 Java(Java 4 = Merlin(鸟),Java 5 = Tiger,Java 6 = 野马(马),Java 7 = 海豚,Java 8 = 没有动物)。

您可以将以下内容添加到您的 POM 文件中:

<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>

并且一旦您使用 JDK 7 API 而希望仅使用 JDK 6 API.

构建将失败

官方也推荐这种用法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 次更新
在Java9这个机制has been radically changed下面的做法:

javac --release N ...

在语义上等同于

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

--release N 方法的主要优点:

  • 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

更新 Java 9 和 Maven
由于版本 3.6.0maven-compiler-plugin 通过其 release 选项提供对 Java 9 的支持:

The -release argument for the Java compiler, supported since Java9

一个例子:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <release>8</release>
    </configuration>
</plugin>