我确定您已经听说过更新到Java 9并不是一件容易的事,甚至可能是不兼容的更新,而且对于大型代码库而言,迁移毫无意义。 这样做之后,我迁移了一个相当大的旧代码库,我可以告诉你,这还不错。 比碰到Java 8确实要花更多的时间,但是要花很多时间。 迁移最重要的是,发现了一些小问题,甚至是一些很小的问题,无论迁移本身如何,都需要解决,我们借此机会做到了。
我在java9.wtf上收集了一些令人惊讶的细节,但是将这七个最大的问题浓缩到了Java 9迁移指南中。 它既是帖子,又是可以返回的资源,因此请将其放在快速拨号上,并在遇到具体问题时进行搜索。 还要注意,尽管您需要了解一些有关模块系统的信息 (这是一个动手指南 ),但这并不是关于模块化应用程序的问题–仅仅是关于使其能够在Java 9上编译和运行。
非法访问内部API
模块系统的最大卖点之一是坚固的封装。 这样可以确保从模块外部无法访问非公共类以及非导出包中的类。 首先,这当然适用于JDK附带的平台模块,其中仅完全支持java。*和javax。*软件包。 另一方面,大多数com.sun。*和sun。*软件包是内部的,因此默认情况下无法访问。
尽管Java 9编译器的行为完全符合您的期望并防止了非法访问,但在运行时却并非如此。 为了提供少量的向后兼容性,它通过允许对内部类的访问,简化了迁移并提高了在Java 8上构建的应用程序在Java 9上运行的机会。 如果使用反射进行访问,则会发出警告。
病征
在针对Java 9进行编译期间,您会看到类似于以下内容的编译错误:
error: package com.sun.java.swing.plaf.nimbus is not visible
import com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel;^(package com.sun.java.swing.plaf.nimbus is declaredin module java.desktop, which does not export it)
1 error
为反射发出的警告如下所示:
Static access to [Nimbus Look and Feel]
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by j9ms.internal.Nimbus(file:...) to constructor NimbusLookAndFeel()
WARNING: Please consider reporting thisto the maintainers of j9ms.internal.Nimbus
WARNING: Use --illegal-access=warn to enable warningsof further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Reflective access to [Nimbus Look and Feel]
修正
依赖内部API的最明显且可持续的解决方案是摆脱它们。 用维护的API替换它们,您还清了一些高风险的技术债务。
如果由于某种原因无法做到这一点,那么下一个最好的办法就是确认依赖关系,并告知模块系统您需要访问它。 为此,您可以使用两个命令行选项:
- 选项–add-exports $ module / $ package = $ readingmodule可用于将$ module的 $ package导出到$ readingmodule 。 因此, $ readingmodule中的代码可以访问$ package中的所有公共类型,而其他模块则不能。 将$ readingmodule设置为 ALL-UNNAMED时,模块图中的所有模块和类路径中的代码都可以访问该包。 在迁移到Java 9的过程中,您将始终使用该占位符。 该选项可用于java和javac命令。
- 这涵盖了对公共类型的公共成员的访问,但是反射可以做得更多:通过大量使用setAccessible(true),它可以与非公共类,字段,构造函数和方法(有时称为Deep Reflection )进行交互,甚至在导出的包中仍然被封装。 java选项–add-opens使用与–add-exports相同的语法,并将包打开以进行深层反射,这意味着所有类型及其成员均可访问,而无论其可见性修饰符如何。
您显然需要–add-exports来安抚编译器,但为运行时收集–add-exports和–add-opens也具有一些优点:
- 运行时的允许行为将在将来的Java版本中更改,因此无论如何您都必须在某些时候进行
- –add-opens使非法反射访问的警告消失
- 正如我将在稍后展示的那样,您可以通过使运行时实际执行强封装来确保没有新的依赖项出现
走得更远
根据Java 9进行编译有助于在项目代码库中查找对内部API的依赖性。 但是您的项目使用的库和框架很可能会造成麻烦。
JDeps是要找到在您的项目和依赖JDK-内部API编译依赖的完美工具。 如果您不熟悉它,我已经写了入门入门。 这是将其用于手头任务的方法:
jdeps --jdk-internals -R --class-path '$libs/*' $project
在这里,$ libs是一个包含所有依赖项的文件夹,$ project是您项目的JAR。 分析输出超出了本文的范围,但并不难–您将进行管理。
找到反射性访问要困难一些。 运行时的默认行为是对首次非法访问软件包的警告一次,这是不够的。 幸运的是,有–illegal-access = $ value选项,其中$ value可以是:
- 允许:允许访问所有JDK内部API,以在类路径上进行编码。 对于反射式访问,将为首次访问每个包装发出单个警告。 (Java 9中的默认值。)
- 警告:行为类似许可证,但每次反射访问都会发出警告。
- 调试:行为像警告,但每个警告中都包含堆栈跟踪。
- 拒绝:对于那些相信强封装的人的选择:
默认情况下,禁止所有非法访问。
特别否认对于寻找反射访问非常有帮助。 一旦收集了所有必需的–add-exports和–add-opens选项,它也是一个很好的默认值。 这样,如果您不注意,就不会出现新的依赖项。
帖子中只有这么多事实-幸运的是,有一本书包含了更多的事实:
Java 9模块系统
- 模块系统的深入介绍:
- 基本概念和高级主题
- 曼宁(Manning)发布:
- 自2017年赛事开始提供抢先体验
- 订阅我的时事通讯以保持关注。
(甚至可以偷看。)
使用代码fccparlog可获得 37%的折扣 !
对Java EE模块的依赖
Java SE中有许多实际上与Java EE相关的代码。 它最终分为以下六个模块:
- 带有javax.activation包的java.activation
- java.corba javax.activity中有,javax.rmi中,javax.rmi.CORBA中,并org.omg。*包
- 带有javax.transaction包的java.transaction
- java.xml.bind和所有javax.xml.bind。*软件包
- 带有javax.jws,javax.jws.soap,javax.xml.soap和所有javax.xml.ws。*包的java.xml.ws
- 带有javax.annotation包的java.xml.ws.annotation
由于各种兼容性原因(其中一个是拆分包,我们将在下面讨论),默认情况下,类路径上的代码看不到这些模块,这会导致编译或运行时错误。
病征
这是java.xml.bind模块中使用JAXBException的类的编译错误:
error: package javax.xml.bind is not visible
import javax.xml.bind.JAXBException;^(package javax.xml.bind is declared in module java.xml.bind,which is not in the module graph)
1 error
如果您无法通过编译器,却忘记了运行时间,则会收到NoClassDefFoundError:
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXBExceptionat monitor.Main.main(Main.java:27)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBExceptionat java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)... 1 more
修正
模块化代码后,可以在模块的声明中声明常规依赖项。 在此之前,–add-modules $ module可以为您解决,确保$ module可用,并且可以将其添加到java和javac中。 如果添加java.se.ee ,则可以访问所有Java EE模块。
分包
这有点棘手…为了增强一致性,不允许一个模块从两个不同的模块读取同一包。 但是,实际的实现更为严格,甚至不允许两个模块包含相同的程序包(已导出或未导出)。 模块系统在该假设下运行,并且每当需要加载一个类时,它都会查找哪个模块包含该程序包,然后在其中查找该类(这将提高类的加载性能)。
为了保护这一假设,模块系统检查是否有两个命名模块拆分了一个程序包和barfs(如果找到)。 但是,在迁移期间,您并没有完全处于这种情况。 您的代码来自类路径,该类路径将其放入所谓的未命名模块中。 为了最大程度地提高兼容性,不对其进行检查,并且不对模块进行任何检查。
现在,在拆分包的情况下,这意味着未发现命名模块(例如,在JDK中)和未命名模块之间的拆分。 如果混用了类加载行为,这听起来可能很幸运:相反:如果在模块和类路径之间拆分包,则对于来自该包的类,类加载将始终且仅查看模块。 这意味着包的类路径部分中的类实际上是不可见的。
病征
症状是,即使绝对存在,也无法加载来自类路径的类,从而导致如下编译错误:
error: cannot find symbolsymbol: class Nonnulllocation: package javax.annotation
或者,在运行时,出现上述NoClassDefFoundErrors。
发生这种情况的一个示例是各种JSR-305实现。 例如,使用注释javax.annotation.Generated(来自java.xml.ws.annotation )和java.annotation.Nonnull(来自com.google.code.findbugs:jsr305 )的项目在编译时会遇到麻烦。 它要么缺少Java EE批注,要么在如上所述添加模块时会遇到拆分包,而看不到JSR 305模块。
修正
迁移路径将有所不同,具体取决于拆分JDK软件包的工件。 在某些情况下,进入随机JDK包的可能不只是某些类,而是整个JDK模块的替代品,例如,因为它覆盖了认可的标准 。 在这种情况下,您正在寻找–upgrade-module-path $ dir选项– $ dir中的模块用于在运行时替换可升级模块。
如果确实只有几个拆分包的类,则长期解决方案是删除拆分。 如果短期内无法实现,则可以使用类路径中的内容修补命名模块。 选项–patch-module $ module = $ artifact会将$ artifact中的所有类合并到$ module中,将split包的所有部分放入同一模块中,从而删除了split。
不过,还有一些注意事项。 首先,打补丁的模块必须实际上将其放入模块图中,为此可能需要使用–add-modules。 然后,它必须有权访问成功运行所需的所有依赖项。 由于命名模块无法从类路径访问代码,因此可能有必要开始创建一些自动模块 ,这超出了本文的范围。
走得更远
通过尝试和错误查找拆分包非常令人不安。 幸运的是, JDeps报告了它们,因此,如果您分析您的项目及其依赖性,输出的第一行将报告已拆分的软件包。 您可以使用与上面相同的命令:
jdeps --jdk-internals -R --class-path '$libs/*' $project
投射到URL类加载器
我刚刚描述的类加载策略是用一种新类型实现的,而在Java 9中,应用程序类加载器就是这种类型。 这意味着它不再是URLClassLoader,因此偶尔的(URLClassLoader)getClass()。getClassLoader()序列将不再执行。 这是另一个典型的示例,其中Java 9在严格意义上是向后兼容的(因为从未指定过它是URLCassLoader),但是仍然可能导致移植挑战。
病征
这是非常明显的。 您将收到ClassCastException抱怨新的AppClassLoader不是URLClassLoader:
Exception in thread "main" java.lang.ClassCastException:java.base/jdk.internal.loader.ClassLoaders$AppClassLoadercannot be cast to java.base/java.net.URLClassLoaderat monitor.Main.logClassPathContent(Main.java:46)at monitor.Main.main(Main.java:28)
修正
类加载器可能已强制转换为特定于URLClassLoader的访问方法。 如果是这样,您只需很小的改动就可以进行迁移。 新的AppClassLoader唯一受支持(因此可访问)的超类型是SecureClassLoader和ClassLoader ,在9中仅添加了一些方法。不过,请看一下,它们可能会满足您的需求。
在运行时图像中拖影
随着JDK的模块化,从根本上改变了运行时映像的布局。 rt.jar,tools.jar和dt.jar等文件不见了; 现在,JDK类已捆绑到jmod文件(每个模块一个)中,jmod文件是一种有目的的未指定文件格式,允许将来进行优化而无需考虑向后兼容性。 而且,JRE和JDK之间的区别消失了。
所有这些都未指定,但这并不意味着根据这些详细信息,这里没有代码。 特别是像IDE这样的工具(尽管它们大多数已经被更新了),这些更改将具有兼容性问题,并且除非更新,否则它们将以无法预测的方式停止工作。
这些更改的结果是,您从系统资源(例如从ClasLoader :: getSystemResource)获取的URL发生了更改。 它过去的格式如下:jar:file:$ javahome / lib / rt.jar!$ path,其中$ path类似于java / lang / String.class。 现在看起来像jrt:/ $ module / $ path。 当然,所有创建或使用此类URL的API均已更新,但是非手工制作这些URL的非JDK代码必须针对Java 9进行更新。
此外,Class :: getResource *和ClassLoader :: getResource *方法不再读取JDK内部资源。 而是使用Module :: getResourceAsStream来访问模块内部资源或创建JRT文件系统,如下所示:
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
fs.getPath("java.base", "java/lang/String.class"));
引导类路径
我在这里很迷茫,因为我从未使用过-Xbootclasspath选项,该选项已被删除。 显然,它的功能已被各种新的命令行选项所取代(此处是JEP 220的解释):
- javac选项–system可用于指定系统模块的备用源
- javac选项–release可用于指定备用平台版本
- 上面提到的java选项–patch-module选项可用于将内容注入到初始模块图中的模块中
新版本字符串
经过20多年的努力,Java终于并正式接受它不再在1.x版中使用。 万岁! 因此,从Java 9开始,系统属性java.version及其兄弟姐妹不再以1.x开头,而是以x开头,即Java 9中的9。
病征
没有明确的症状-如果某些实用程序功能确定错误的版本,几乎一切都可能出错。 不过,找到它并不难。 对以下字符串的全文搜索应导致所有特定于版本字符串的代码:java.version,java.runtime.version,java.vm.version,java.specification.version,java.vm.specification.version。
修正
如果您愿意将项目的要求提高到Java 9,则可以避开整个系统属性的探测和解析,而可以使用新的Runtime.Version类型 ,这使所有这些操作变得更加容易。 如果您想保持与Java 9之前版本的兼容性,您仍然可以通过创建多发行版JAR来使用新的API。 如果这还是不可能的话,看来您实际上必须编写一些代码(很多!)并根据主要版本进行分支。
摘要
现在,您知道如何使用内部API(–add-export和–add-opens),如何确保存在Java EE模块(–add-modules)以及如何处理拆分包(–patch-module)。 这些是您在迁移过程中最可能遇到的问题。 URLClassLoader转换为较不常见且也较不易于修复而无法访问有问题的代码,这是由于新的运行时映像布局和资源URL,已删除的-Xbootclasspath和新版本字符串导致的问题。
知道如何解决这些问题将为您提供很好的机会来克服所有的迁移难题,并使您的应用程序在Java 9上编译和运行。如果没有,请查看JEP 261的Risks and Assumptions部分 ,其中列出了其他一些潜在的陷阱。
如果您不知所措,请等待我的下一篇文章,其中提供有关如何将这些单独的修补程序纳入全面的迁移策略的一些建议,例如,通过包括构建工具和持续集成。 或索取我的书 ,在那里我将解释所有这些以及更多内容。
翻译自: https://www.javacodegeeks.com/2017/07/java-9-migration-guide-seven-common-challenges.html