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

Posted

技术标签:

【中文标题】Eclipse/Idea 忽略了 Maven Java 版本配置【英文标题】:Maven Java Version Configuration ignored by Eclipse/Idea 【发布时间】:2016-06-25 03:17:40 【问题描述】:

我有:

<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。在没有任何警告的情况下构建该死的东西。

【问题讨论】:

删除&lt;pluginManagement&gt;,你会看到高高在上的错误。 一般来说,源/目标经常被误解,它们检查语法,而不是使用的 API。您应该始终使用bootstrap 选项来确保编译并与您的目标java 版本保持一致。或者,如果您在使用目标 6 编译时使用 java 7/8 API,则可以使用 animal-sniffer maven 插件来中断构建 此外,您应该使用工具链来正确定义使用的 JDK,独立于用于运行 Maven 本身的 JDK... @Tunaki:很遗憾,没有。我没有。 【参考方案1】:

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

使用source/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

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

Since JDK 7, javac prints a warning 如果source/target 未与bootclasspath 选项结合使用。在这些情况下,bootclasspath 选项是指向所需目标 Java 版本的 rt.jar 的关键选项(因此,您需要在机器中安装目标 JDK)。因此,javac 将有效地针对良好的 Java API 进行编译。

但这可能还不够!

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

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

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

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

但是..可能还不够!

来自甲骨文官方documentation

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

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

生成适用于特定 JDK 及更高版本的类文件的最可靠方法是使用感兴趣的最旧的 JDK 编译源文件。除此之外,必须设置引导类路径以实现对旧 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 级别。 但它也使用 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 传统上以 different animals 命名不同版本的 Java(Java 4 = Merlin(鸟),Java 5 = Tiger,Java 6 = Mustang(马),Java 7 = Dolphin,Java 8 = no animal) .

您可以将以下内容添加到您的 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也推荐这种用法

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


Java 9 更新 在 Java 9 中这种机制has been radically changed 采用以下方法:

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 9 的支持:

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

一个例子:

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

【讨论】:

关于这个答案的几个小问题。 -source 选项控制编译器接受的源代码的语言级别-target 选项控制编译器发出的类文件版本。看起来它们是独立的,但实际上并非如此。依赖关系可能非常复杂。例如,编译 lambda 需要 invokedynamic 字节码,所以 -source 1.8 -target 1.6 根本不起作用。 对库 API 的强调恰到好处。某些语言功能,例如 try-with-resources,需要特定的库 API,例如 AutoCloseable。因此,-source 1.7 -target 1.6 可能会生成一个有效的 1.6 类文件,但它不能在 JDK 6 上运行。这里的答案是非常正确的,即 -source x -target x 不足以交叉编译到早期的 JDK 版本。注意库 API 问题也很重要。 非常正确,Java 9 中会有一些变化。另见JEP 247,它引入了一个新的-release 选项来替换旧选项。尚未最终确定,仍有可能更改等。 我可能会迟到回答,但 maven 也提供了工具链:这允许您使用 JDK8 来执行 maven 和 JDK6 来编译。从某种意义上说,这比使用源/目标更好:maven.apache.org/guides/mini/guide-using-toolchains.html 完美答案。正是我想要的。特别区分源/目标和 API。如果可以的话,我会再次投票多次。

以上是关于Eclipse/Idea 忽略了 Maven Java 版本配置的主要内容,如果未能解决你的问题,请参考以下文章

配置Maven环境变量-Eclipse/Idea添加Maven

Eclipse/IDEA创建maven web工程

Eclipse/IDEA创建maven web工程

JAVA生成(可执行)Jar包的全面详解说明 [打包][SpringBoot][Eclipse][IDEA][Maven][Gradle][分离][可执行]

idea 怎么引入在pom.xml的jar

Maven 发布插件 - tagBase 被忽略