Eclipse/Idea 忽略的 Maven Java 版本配置 [英] Maven Java Version Configuration ignored by Eclipse/Idea

查看:73
本文介绍了Eclipse/Idea 忽略的 Maven Java 版本配置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有:

<插件管理><插件><插件><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><配置><来源>1.6</来源><目标>1.6</目标></配置></插件></插件></pluginManagement></build>

但我可以毫无问题地声明:

public enum DirectoryWatchService {实例;私有 java.util.Optional测试;私有 java.nio.file.Files 文件;}

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

解决方案

您遇到了对 source/target 选项的交叉编译误解.在您的类路径(JDK 7 或 8)中使用主要版本,但希望针对次要版本(在您的情况下为 6)进行编译.
编译没问题,但您会在运行时出现错误(即NoClassDefFoundErrorNoSuchMethodError,可能也是更通用的 LinkageError).

使用/target,Java 编译器可用作交叉编译器,以生成可在实现 Java SE 规范早期版本的 JDK 上运行的类文件.

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

编译器负责字节码生成,sourcetarget 用于在交叉编译期间生成兼容的字节码.但是,Java API 不由编译器处理(它们作为 JDK 安装的一部分提供,即著名的 rt.jar 文件).编译器没有任何 API 知识,它只是针对当前的 rt.jar 进行编译.因此,当使用 JDK 1.7 以 target=1.6 进行编译时,编译器仍将指向 JDK 7 rt.jar.

那么,我们如何才能真正进行正确的交叉编译?

自 JDK 7 起,javac 会在 source<的情况下打印警告/code>/target 不能与 bootclasspath 选项结合使用.在这些情况下,bootclasspath 选项是指向所需目标 Java 版本的 rt.jar 的关键选项(因此,您需要将目标 JDK 安装在您的机器).因此,javac 将有效地针对良好的 Java API 进行编译.

但这可能还不够!

并非所有 Java API 都来自 rt.jar.libext 文件夹提供了其他类.另一个javac 选项用于此目的,extdirs.来自 Oracle 官方文档

<块引用>

如果您要交叉编译(针对不同 Java 平台实现的引导程序和扩展类编译类),则此选项指定包含扩展类的目录.

因此,即使使用 source/targetbootclasspath 选项,我们仍然可能在交叉编译过程中遗漏一些东西,正如在官方示例javac 文档.

<块引用>

Java 平台 JDK 的 javac 默认情况下也会针对其自己的引导程序类进行编译,因此我们需要告诉 javac 改为针对 JDK 1.5 引导程序类进行编译.我们使用 -bootclasspath 和 -extdirs 来做到这一点.如果不这样做,可能会允许针对 1.5 虚拟机上不存在的 Java 平台 API 进行编译,并且会在运行时失败.

但是……可能还不够!

来自 Oracle 官方文档

<块引用>

即使 bootclasspath 和 -source/-target 都针对交叉编译进行了适当设置,编译器内部契约(例如匿名内部类的编译方式)可能在 JDK 1.4.2 中的 javac 和 javac 之间有所不同在使用 -target 1.4 选项运行的 JDK 6 中.

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

<块引用>

生成适用于特定 JDK 及更高版本的类文件的最可靠方法是使用感兴趣的最早的 JDK 编译源文件.除此之外,必须设置引导类路径,以便与较旧的 JDK 进行健壮的交叉编译.

所以,这真的不可能吗?

Spring 4 目前支持 Java 6、7 和 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水平.
但它也使用 Java 7 和 Java 8 API!因此,不使用 bootclasspath 选项.可选,Stream 和许多其他 Java 8 API 被使用.然后,仅当在运行时检测到 JRE 7/8 时才根据 Java 7 或 Java 8 API 注入 bean:聪明的方法!

那么 Spring 是如何保证 API 兼容性的呢?

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

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

<插件><插件><groupId>org.codehaus.mojo</groupId><artifactId>animal-sniffer-maven-plugin</artifactId><version>1.14</version><配置><签名><groupId>org.codehaus.mojo.signature</groupId><artifactId>java16</artifactId><version>1.0</version></签名></配置><执行><执行><id>ensure-java-1.6-class-library</id><阶段>验证</阶段><目标><目标>检查</目标></目标></执行></执行></插件></插件></build>

一旦您使用 JDK 7 API 而希望仅使用 JDK 6 API,构建就会失败.

官方maven-compiler-plugin

的 "noreferrer">source/target 页面<块引用>

注意:仅设置 target 选项并不能保证您的代码实际上在指定版本的 JRE 上运行.陷阱是无意中使用了仅存在于后续 JRE 中的 API,这会使您的代码在运行时因链接错误而失败.为避免此问题,您可以配置编译器的启动类路径以匹配目标 JRE,或使用 Animal Sniffer Maven 插件来验证您的代码没有使用意外的 API.

<小时>

Java 9 更新
在 Java 9 中,这种机制已经彻底改变 到以下方法:

javac --release N ...

在语义上等同于

javac -source N -target N -bootclasspath rtN.jar ...

<块引用>

  • 有关 javac 的早期版本 API 的信息

    • 以压缩方式存储
    • 仅提供平台中立的 Java SE N 和 JDK N 导出的 API
  • -source/-target
  • 相同的一组发布值N
  • 不兼容的选项组合被拒绝

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

<块引用>
  • 用户无需管理存储旧 API 信息的工件
  • 应该不再需要使用诸如 Maven 插件 Animal Sniffer 之类的工具
  • 可能会使用比旧版本中的 javac 更新的编译习惯用语

    • 错误修复
    • 速度提升

<小时>

Java 9 和 Maven 更新
3.6.0 版本开始,maven-compiler-plugin 通过其 release 选项:

<块引用>

Java 编译器的 -release 参数,自 Java9 起支持

示例:

<artifactId>maven-compiler-plugin</artifactId><version>3.6.0</version><配置><release>8</release></配置></插件>

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.

解决方案

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

这篇关于Eclipse/Idea 忽略的 Maven Java 版本配置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆