Eclipse/Idea 忽略的 Maven Java 版本配置 [英] Maven Java Version Configuration ignored by Eclipse/Idea
问题描述
我有:
<插件管理><插件><插件><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)进行编译.
编译没问题,但您会在运行时出现错误(即NoClassDefFoundError
或 NoSuchMethodError
,可能也是更通用的 LinkageError
).
使用源
/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
.
那么,我们如何才能真正进行正确的交叉编译?
自 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
/target
和 bootclasspath
选项,我们仍然可能在交叉编译过程中遗漏一些东西,正如在官方示例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
/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 传统上以不同的动物(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,构建就会失败.
的 "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屋!