从 Java 8 转换到 Java 11

没有一个大小拟合的解决方案,用于将代码从 Java 8 转换为 Java 11。 对于非简单应用程序,从 Java 8 迁移到 Java 11 可能是大量的工作。 潜在的问题包括删除了 API、弃用的包、内部 API 的使用、对类加载程序所做的更改以及垃圾回收的更改。

通常,这些方法是尝试在 Java 11 上运行,而无需重新编译,或先使用 JDK 11 进行编译。 如果目标是尽快启动并运行应用程序,则尝试在 Java 11 上运行通常是最佳方法。 对于库,目标是发布使用 JDK 11 编译和测试的项目。

迁移到 Java 11 值得努力。 添加了新功能,自 Java 8 以来已增强功能。 这些功能和增强功能可提高启动、性能、内存使用率,并更好地与容器集成。 此外,API 还有一些添加和修改,可提高开发人员工作效率。

本文档涉及用于检查代码的工具。 它还涵盖可能遇到的问题以及解决这些问题的建议。 还应参阅其他指南,例如 Oracle JDK 迁移指南。 此处未介绍如何使现有代码 模块化

工具箱

Java 11 有两个工具 (jdeprscanjdeps),可用于探查潜在问题。 这些工具可以针对现有类或 jar 文件运行。 无需重新编译即可评估转换工作。

jdeprscan 可查看是否使用了已弃用或已删除的 API。 使用已弃用的 API 不是一个阻碍性的问题,但需要进行研究。 是否有更新的 jar 文件? 是否需要记录问题才能解决已弃用 API 的使用问题? 使用已删除的 API 是在尝试在 Java 11 上运行之前必须解决的阻止问题。

jdeps,它是 Java 类依赖项分析器。 与选项一起使用 --jdk-internals 时, jdeps 会告诉你哪个类取决于哪个内部 API。 可以在 Java 11 中继续使用内部 API,但替换用法应是优先级。 OpenJDK Wiki 页面 Java 依赖项分析工具 已建议替换一些常用的 JDK 内部 API。

Gradle 和 Maven 都有 jdepsjdeprscan 插件。 建议将这些工具添加到生成脚本。

Java 编译器本身 javac 是工具箱中的另一个工具。 从 jdeprscanjdeps 获取的警告和错误将来自编译器。 使用 jdeprscanjdeps 的优点是,你可以对现有 jar 和类文件(包括第三方库)运行这些工具。

jdeprscanjdeps 不能做的是发出关于使用反射访问封装 API 的警告。 反射访问在运行时进行检查。 最终,必须在 Java 11 上运行代码才能确定。

使用 jdeprscan

使用 jdeprscan 的最简单方法是从现有构建中为其提供 jar 文件。 还可以为它提供一个目录,例如编译器输出目录或单个类名。 使用此选项 --release 11 获取已弃用 API 的最完整列表。 若要确定要采用的已弃用 API 的优先级,请将设置回退到 --release 8。 在 Java 8 中弃用的 API 可能比最近弃用的 API 更早删除。

jdeprscan --release 11 my-application.jar

如果在解决依赖类时遇到问题, jdeprscan 工具将生成错误消息。 例如,error: cannot find class org/apache/logging/log4j/Logger。 建议将依赖类添加到 --class-path 或使用应用程序类路径,但该工具将继续扫描而不使用它。 参数为 --class-path。 类路径参数的其他变体不起作用。

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

此输出告诉我们, com.company.Util 该类正在调用该类的已弃用构造函数 java.lang.Double 。 javadoc 将推荐可用于替代已弃用 API 的新 API。 无论如何都无法解决“error: cannot find class sun/misc/BASE64Encoder”问题,因为它是已删除的 API。 从 Java 8 起,应该使用 java.util.Base64

运行 jdeprscan --release 11 --list 以获取自 Java 8 以来已弃用的 API。 若要获取已删除的 API 列表,请运行 jdeprscan --release 11 --list --for-removal

使用 jdeps

使用 jdeps,并通过 --jdk-internals 选项查找对 JDK 内部 API 的依赖项。 此示例需要命令行选项 --multi-release 11 ,因为 log4j-core-2.13.0.jar一个多发行 jar 文件。 如果没有此选项,jdeps 在找到多版本 jar 文件时将发出警告。 该选项指定要检查的类文件的哪个版本。

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

输出提供有关消除 JDK 内部 API 的使用的一些好建议! 如果可能,建议替换 API。 封装包的模块的名称在括号中提供。 如有必要显式地打破封装,模块名称可以与 --add-exports--add-opens 一同使用。

使用 sun.misc.BASE64Encodersun.misc.BASE64Decoder 会导致 Java 11 中出现 java.lang.NoClassDefFoundError。 必须修改使用这些 API 的代码才能使用 java.util.Base64

尝试消除来自模块 jdk.unsupported 的任何 API 的使用。 此模块中的 API 将引用 JDK 增强建议 (JEP) 260 作为建议的替换。 简言之,JEP 260 表示,在更换 API 可用之前,将支持使用内部 API。 尽管代码可能使用 JDK 内部 API,但它至少会持续运行一段时间。 请查看 JEP 260,因为它确实指出了某些内部 API 的替换。 变量句柄 可用于代替某些 sun.misc.Unsafe API,例如。

除了扫描 JDK 内部 API 的使用情况,jdeps 还可以执行其他操作。 它是用于分析依赖项和生成模块信息文件的有用工具。 有关详细信息,请查看 文档

使用 javac

使用 JDK 11 进行编译需要更新才能生成脚本、工具、测试框架和包含的库。 使用 -Xlint:unchecked 选项可以获取有关 javac 的 JDK 内部 API 使用和其他警告的详细信息。 可能还需要使用 --add-opens--add-reads 向编译器公开封装包(请参阅 JEP 261)。

库可以考虑打包为 多版本 jar 文件。 多版本 jar 文件允许您使用同一个 jar 文件支持 Java 8 和 Java 11 运行时。 它们确实为构建增加了复杂性。 如何生成多版本 jar 超出了本文档的范围。

在 Java 11 上运行

大多数应用程序应在 Java 11 上运行,而无需修改。 首先尝试在 Java 11 上运行,而无需重新编译代码。 直接运行的目的是查看执行时会出现哪些警告和错误。 此方法可以
让应用程序在 Java 11 上更快地运行,因为可以尽量减少那些必须完成的关注事项。

你可能会遇到的大多数问题都可以解决,而无需重新编译代码。 如果代码中必须修复问题,请进行修复,但继续使用 JDK 8 进行编译。 如果可能,请在使用 JDK 11 进行编译之前,让应用程序使用 java 版本 11 运行

检查命令行选项

在 Java 11 上运行之前,请快速扫描命令行选项。 已删除的选项 将导致 Java 虚拟机(JVM)退出。 使用 GC 日志记录选项时,此检查尤其重要,因为它们自 Java 8 起已发生重大变化。 JaCoLine 工具非常适合用于检测命令行选项的问题。

检查第三方库

问题的潜在来源是你无法控制的第三方库。 可以主动将第三方库更新为最新版本。 也可查看运行应用程序时哪些库未使用,仅更新那些必需的库。 将所有库更新到最新版本的问题在于,如果应用程序中出现一些错误,则很难找到根本原因。 错误是否由于某些更新的库而发生? 还是由运行时中的一些更改引起的错误? 更新仅必要的内容的问题在于,这一方法可能需要多次迭代才能解决。

此处的建议是尽可能少进行更改,并 单独更新第三方库。 如果确实更新了第三方库,您通常会希望使用与 Java 11 兼容的最新且最好的版本。 根据当前版本的落后程度,可能需要采取更谨慎的方法并升级到第一个 Java 9+ 兼容版本。

除了查看发行说明,还可以使用 jdepsjdeprscan 来评估 jar 文件。 此外,OpenJDK 质量组还维护了一个 质量外展 Wiki 页面,其中列出了针对 OpenJDK 版本测试许多免费开放源代码软件(FOSS)项目的状态。

显式设置垃圾回收

并行垃圾回收器(Parallel GC)是 Java 8 中的默认 GC。 如果应用程序使用的是默认值,则应使用命令行选项 -XX:+UseParallelGC显式设置 GC。 Java 9 中的默认值已更改为 Garbage First 垃圾回收器 (G1GC)。 为了对 Java 8 上运行的应用程序与 Java 11 上运行的应用程序进行公平比较,GC 设置必须相同。 在 Java 11 上验证应用程序之前,应推迟试验 GC 设置。

显式设置默认选项

如果在 HotSpot VM 上运行,则设置命令行选项 -XX:+PrintCommandLineFlags 将转储 VM 设置的选项值,尤其是 GC 设置的默认值。 在 Java 8 上运行时使用此标志,并在 Java 11 上运行时使用打印出的选项。 在大多数情况下,默认值与 8 到 11 相同。 但使用 8 中的设置可确保一致性。

建议设置命令行选项 --illegal-access=warn 。 在 Java 11 中,使用反射访问 JDK 内部 API 将导致 非法反射访问警告。 默认情况下,仅针对第一个非法访问发出警告。 设置 --illegal-access=warn 会导致系统对每一次非法反射访问发出警告。 如果将选项设置为警告,你会发现更多非法访问的情况。 但是,你也会收到大量冗余警告。
应用程序在 Java 11 上运行后,设置为 --illegal-access=deny 模拟 Java 运行时的未来行为。 从 Java 16 开始,默认值为 --illegal-access=deny

ClassLoader 注意事项

在 Java 8 中,可以将系统类加载程序强制转换为 URLClassLoader。 这通常是由希望在运行时将类注入类路径的应用程序和库完成的。 类加载程序层次结构在 Java 11 中已更改。 系统类加载程序(也称为应用程序类加载程序)现在是内部类。 强制转换为 URLClassLoader 会在运行时引发 ClassCastException。 Java 11 无法通过 API 在运行时动态增强 classpath,但可以通过反射来实现这一点,它会显示有关如何使用内部 API 的显著警告。

在 Java 11 中,启动类加载程序仅加载核心模块。 如果创建一个父级为 null 的类加载器,则可能无法找到所有平台类。 在 Java 11 中,在这种情况下,需要传递 ClassLoader.getPlatformClassLoader() 而不是 null 作为父类加载程序。

区域设置数据更改

Java 11 中用于区域设置数据的默认源已通过 JEP 252 更改为 Unicode 联体的公共区域设置数据存储库。 这可能会影响本地化格式。 根据需要设置系统属性 java.locale.providers=COMPAT,SPI 以还原为 Java 8 区域设置行为。

潜在问题

下面是可能会遇到的一些常见问题。 请按照链接获取有关这些问题的更多详细信息。

无法识别的选项

如果删除了命令行选项,应用程序将打印 Unrecognized option:Unrecognized VM option 后跟有问题的选项的名称。 无法识别的选项将导致 VM 退出。 已弃用但未删除的选项将生成 VM 警告

通常,删除的选项没有替换,唯一的追索权是从命令行中删除该选项。 垃圾回收日志记录的选项是一个例外。 GC 日志记录在 Java 9 中 重新实施 ,以使用 统一 JVM 日志记录框架。 请参阅 Java SE 11 工具参考的允许通过 JVM 统一日志记录框架进行日志记录部分中的“表2-2 将旧的垃圾回收日志记录标志映射到 Xlog 配置”。

VM 警告

使用已弃用的选项将生成警告。 已替换或不再有用时,将弃用某个选项。 与 删除的选项一样,应从命令行中删除这些选项。 警告 VM Warning: Option <option> was deprecated 表示仍支持该选项,但将来可能会删除该支持。 不再支持的选项,将生成警告 VM Warning: Ignoring option。 不再支持的选项对运行时没有影响。

网页 VM 选项资源管理器 提供了自 JDK 7 以来已添加到 Java 或从 Java 中删除的选项的详尽列表。

错误:无法创建 Java 虚拟机

当 JVM 遇到 无法识别的选项时,将输出此错误消息。

警告:发生非法的反射访问操作

当 Java 代码使用反射来访问 JDK 内部 API 时,运行时将发出非法反射访问警告。

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

这意味着模块尚未导出通过反射访问的包。 该包封装在模块中,基本上是供内部使用的API。 忽略该警告可以作为在 Java 11 上开始启动和运行的一个初步尝试。 Java 11 运行时允许反射访问,以便旧代码可以继续工作。

若要解决此警告,请查找未使用内部 API 的更新代码。 如果无法使用更新的代码解决此问题,可以使用命令行选项--add-exports--add-opens来打开对包的访问。 这些选项允许从另一个模块访问一个模块的未导出类型。

--add-exports 选项允许目标模块访问源模块的命名包 的公共 类型。 有时,代码会使用 setAccessible(true) 来访问非公共成员和 API。 这称为 深度反射。 在这种情况下,使用 --add-opens 来授予代码对包非公共成员的访问权限。 如果不确定是使用 --add-exports 还是 --add-opens,请从 --add-exports 开始。

--add-exports--add-opens选项应被视为解决方法,而不是长期解决方案。 使用这些选项会中断模块系统的封装,这意味着 JDK 内部 API 不被使用。 如果删除或更改了内部 API,应用程序将失败。 反射访问将在 Java 16 中被拒绝,除非通过命令行选项(如 --add-opens)启用访问。 要模拟未来的行为,请在命令行中设置--illegal-access=deny

由于sun.nio.ch模块未导出java.base包,因此会在上述示例中发出警告。 换句话说,模块exports sun.nio.ch;module-info.java文件中没有java.base。 这可以通过 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED 来解决。 模块中未定义的类隐式属于 未命名 的模块,字面上命名 ALL-UNNAMED

java.lang.reflect.InaccessibleObjectException

此异常表示你正在尝试对封装类的字段或方法调用 setAccessible(true) 。 也可能会收到一个“非法的反射访问”警告。 使用 --add-opens 选项可以让代码访问包的非公共成员。 异常消息会告知你,模块未将包打开到试图调用 setAccessible 的模块。 如果模块是“未命名模块”,请将 UNNAMED-MODULE 作为 --add-opens 选项中的目标模块使用。

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

NoClassDefFoundError 很可能是由拆分包或引用已删除的模块引起的。

拆分包导致的 NoClassDefFoundError

当一个被拆分的包在多个库中被发现时。 拆分包问题的症状是,你知道某个类会在 class-path 上,但找不到该类。

仅当使用模块路径时,才会发生此问题。 Java 模块系统通过将包限制为一个 命名 模块来优化类查找。 执行类查找时,运行时优先选择模块路径而不是类路径。 如果包在模块和类路径之间拆分,则仅使用该模块执行类查找。 这可能会导致 NoClassDefFound 错误。

检查拆分包的一种简单方法是将模块路径和类路径插入 jdeps ,并使用应用程序类文件的路径作为 <路径>。 如果有拆分包,jdeps 将输出警告: Warning: split package: <package-name> <module-path> <split-path>

可以通过使用 --patch-module <module-name>=<path>[,<path>] 将拆分包添加到命名模块来解决此问题。

使用 Java EE 或 CORBA 模块导致的 NoClassDefFoundError

如果应用程序在 Java 8 上运行,但会引发java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException,则应用程序很可能使用 Java EE 或 CORBA 模块中的包。 这些模块已在 Java 9 中弃用,并在 Java 11 中删除

若要解决此问题,请将运行时依赖项添加到项目。

已删除的模块 受影响的包 建议的依赖项
适用于 XML Web 服务的 Java API (JAX-WS) java.xml.ws JAX WS RI 运行环境
用于 XML 绑定的 Java 体系结构 (JAXB) java.xml.bind JAXB 运行时
JavaBeans 激活框架 (JAV) java.activation JavaBeans (TM) 激活框架
常见批注 java.xml.ws.annotation Javax 批注 API
通用对象请求代理体系结构 (CORBA) java.corba GlassFish CORBA ORB
Java 事务 API (JTA) java.transaction Java 事务 API

-Xbootclasspath/p 不再是受支持的选项

已删除对 -Xbootclasspath/p 的支持。 请改用 --patch-moduleJEP 261 中介绍了 --patch-module 选项。 查找标为“修补模块内容”的部分。 --patch-module 可用于 javacjava 来替代或扩充模块中的类。

实际上,-patch-module 的作用是将修补模块插入模块系统的类查找中。 模块系统将首先从修补模块中获取类。 这与在 Java 8 中前置 bootclasspath 的效果相同。

UnsupportedClassVersionError

此异常意味着你尝试在早期版本的 Java 上运行使用更高版本的 Java 编译的代码。 例如,你在 Java 11 上运行,其中包含使用 JDK 13 编译的 jar。

Java 版本 类文件格式版本
8 52
9 53
10 54
11 55
12 56
13 57

后续步骤

应用程序在 Java 11 上运行后,请考虑将库移出类路径并移到模块路径上。 查找应用程序所依赖的库的更新版本。 选择模块化库(如果可用)。 尽可能多地使用模块路径,即使不打算在应用程序中使用模块。 使用模块路径的类加载性能比使用类路径更好。