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。在没有任何警告的情况下构建该死的东西。
【问题讨论】:
删除<pluginManagement>
,你会看到高高在上的错误。
一般来说,源/目标经常被误解,它们检查语法,而不是使用的 API。您应该始终使用bootstrap 选项来确保编译并与您的目标java 版本保持一致。或者,如果您在使用目标 6 编译时使用 java 7/8 API,则可以使用 animal-sniffer maven 插件来中断构建
此外,您应该使用工具链来正确定义使用的 JDK,独立于用于运行 Maven 本身的 JDK...
@Tunaki:很遗憾,没有。我没有。
【参考方案1】:
您遇到了source
/target
选项的交叉编译误解。在您的类路径(JDK 7 或 8)中使用主要版本,但希望针对次要版本(在您的情况下为 6)进行编译。
编译会没问题,但在运行时会出现错误(即NoClassDefFoundError
或NoSuchMethodError
,也可能是更通用的LinkageError
)。
使用source
/target
,Java 编译器可用作交叉编译器,以生成可在实现早期版本的 Java SE 规范的 JDK 上运行的类文件。
普遍认为使用两个编译器选项就足够了。但是,source
选项指定我们正在编译的版本,而target
选项指定要支持的最低 Java 版本。
编译器处理字节码生成,source
和target
用于在交叉编译期间生成兼容的字节码。但是,编译器不处理 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
/target
和 bootclasspath
选项,我们仍然可能在交叉编译期间遗漏一些东西,正如 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
/target
和bootclasspath
的灵活性。 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 Plugin 来验证您的代码没有使用非预期的 API。
Java 9 更新 在 Java 9 中这种机制has been radically changed 采用以下方法:
javac --release N ...
将在语义上等价于
javac -source N -target N -bootclasspath rtN.jar ...
以压缩方式存储 仅提供与平台无关的 Java SE N 和 JDK N 导出的 API 与
javac
提供的早期版本 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
JAVA生成(可执行)Jar包的全面详解说明 [打包][SpringBoot][Eclipse][IDEA][Maven][Gradle][分离][可执行]