您遇到了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 插件来验证您的代码没有使用意外 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 方法的主要优点:
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>