专业的编程技术博客社区

网站首页 > 博客文章 正文

将现有代码库迁移到Java 9及以上版本时要面对的兼容性挑战

baijin 2024-08-28 11:24:52 博客文章 3 ℃ 0 评论

一 使用JEE模块

许多Java SE代码与Java EE/Jakarta EE(下文将两者缩写为JEE)相关,我们立刻能想到的有CORBA、Java Architecture for XMLBinding(JAXB)Java API for XML Web Services(JAX-WS)

这些API与其他API属于表6-1中列出的模块。很遗憾,这并不是一个简短的旁注可以解释清楚的。当你试着编译或运行一些代码,而这些代码所依赖的类属于这些模块时,模块系统会报告模块图中缺失这些模块。

6-1 6JEE模块。

这是在Java 9中对某个类进行编译时所遇到的编译错误。该类使用了java.xml.bind模块中的JAXBException

如果让它通过编译却忘了告知运行时,你将得到NoClassDefFoundError

发生了什么?为什么标准化的Java API对于类路径中的代码不可用?如何解决这个问题?

1 为什么JEE模块很特殊

Java SE包含一些由授权标准以及独立技术构成的包。这些技术是在JavaCommunity Process(JCP)之外开发的,这通常是因为它们依赖于由其他组织监管的标准。

相关的例子如由万维网联盟(W3C)WebHypertext Application Technology Working Group(WHATWG)开发的文档对象模型(DOM),以及Simple API for XML(SAX)

由于历史原因,Java运行时环境(JRE)发布时带有这些技术的实现,但是用户可以在JRE之外单独升级它们。这可以通过授权标准覆盖机制来实现。

类似地,应用程序服务器经常通过提供自己的实现来扩展或升级CORBA、JAXB或者JAX-WS API,以及(java.activation中的)JAF或者(java.transaction中的)JTA。最终,java.xml.ws.annotation模块包含了javax.annotation包。它经常被不同的JSR 305实现扩展,后者因其与null相关的注解而闻名。

在上述对随Java发布的API进行扩展或替换的例子中,有一个技巧是使用完全相同的包名或类名,这样,这些类就会从外部JAR而非内建的JAR中加载。用模块系统的术语来说,这叫作包分裂,即同一个包被拆分到不同的模块中,或者拆分到模块和类路径中。

包分裂的终结

包分裂在Java 9及以上版本中不再可用。后文将讨论相关细节,目前仅需了解在类路径中,随Java发布的包中的类是不可见的。

1)如果Java包含了一个具有相同完全限定名的类,它将被加载。

2)如果该包的Java内建版本不包含所需要的类,那么不论该类是

否存在于类路径中,结果都会是编译错误或者刚刚展示的NoClassDefFoundError

这是所有模块中所有包的通用机制:在模块和类路径之间将它们分裂会让类路径部分不可见。与其他模块不同,这6个JEE模块的独特之处是,它们通常使用包分裂的方式进行扩展或升级。

为了让应用程序服务器和类似JSR 305实现的库在不需要大量配置的情况下继续工作,人们不得不妥协:针对类路径中的代码,Java 9Java10默认不会解析JEE模块,这意味着它们不会被放入模块图,因此这些JEE模块是不可用的。

这在自身实现了JEE API的应用程序中效果很好,但在依靠JDK内建版本的应用程序中不怎么样。在不做进一步配置的情况下,类路径中的代码如果使用了那6个JEE模块所提供的类型,将无法编译和运行。

为了解决这个难题,并且恰当地将Java SEJEE分开,Java 9弃用了这些模块,Java 11则删除了它们。随着它们的删除,像wsgenxjc这样的命令行工具也不再同JDK一起发布。

2 人工解析JEE模块

当由于缺少JEE API而遇到编译时或运行时错误,或者JDeps分析(参见附录D)显示应用程序依赖于JEE模块时,该怎么办?可以采用以下3种方法。

1)如果你的应用程序在应用程序服务器中运行,它会提供这些API的实现,此时你应该不会遇到运行时错误。这与设置有关,你也许需要修复构建错误——虽然另外两个方案也需如此。

2)选择相关API的第三方实现,将其作为依赖添加到项目中。由于JEE模块在默认情况下不会得到解析,因此编译时和运行时可以正常使用这些第三方实现。

3)在Java 9Java 10中,用--add-modules选项添加平台模块。因为Java 11删除了JEE模块,所以这个办法不适用于Java 11

本节最开始的例子尝试使用java.xml.bind模块中的JAXBException。下面展示了如何通过--add-modules选项让这个模块对编译可用。

代码完成编译和打包后,为方便执行,你需要再次添加这个模块。

如果依赖于一些JEE API,添加java.se.ee模块会比逐个添加单个模块简单一些。它让所有6个JEE模块全部可用,将事情简化。(它是如何让这些模块可用的?后文将会提到。)

要点 建议你认真考虑将必要API的第三方实现添加为项目的常规依赖,以替代--add-modules。后文将讨论使用命令行选项的缺点,所以在沿着这条路继续向前之前,请先阅读这一节。同时,由于Java 11删除了JEE模块,因此你早晚会需要一个第三方实现。

只有非模块化代码才需要人工添加JEE模块。一旦代码被模块化,JEE模块就不再特别:你可以像对待任何其他模块一样对它们进行依赖,它们也会像其他模块一样被解析,至少在被删除之前是这样。

第三方JEE实现

比较和讨论各个JEE API的第三方实现会过度偏离模块系统这一主题,所以本书不会这样做。但是,JEP 320Stack Overflow的官方网站列出了一些选项。

3 JEE模块的第三方实现

也许你一直在使用授权标准覆盖机制来更新标准和独立技术。你也许会好奇,在使用模块后,这个机制发生了什么变化。正如你可能已经猜到的,它被删除了并被新事物取代。

编译器和运行时都提供--upgrade-module-path选项,用于接受一个目录列表。该目录列表的格式与模块路径的参数相同。模块系统在创建模块图时,会在这些目录中查找工件并用它们替换可升级模块。6个JEE模块永远可以升级。
1)java.activation

2)java.corba

3) java.transaction

4) java.xml.bind

5) java.xml.ws

6) java.xml.ws.annotation

JDK的供应商会使更多模块可升级。例如在Oracle JDK上,java.jnlp模块就是如此。此外,通过jlink链接到模块图的应用程序模块也始终是可升级的。

在升级模块路径上,JAR不必是模块化的。如果缺少模块描述符,它们

将被转换为自动模块,并且仍然可以替换Java模块。

二 转化为URLClassLoader

在Java 9或更高版本上运行项目时,可能会遇到类似下面示例的类转换异常。示例中,JVM抱怨无法将jdk.internal.loader.ClassLoaders.AppClassLoader的实例转化为URLClassLoader

上述新类型是什么?为什么代码无法运行?下面找找原因。在此过程中,你将了解Java 9如何通过改变类加载行为提高启动性能。因此,即使你在项目中没有遇到此类问题,它仍然是你强化Java知识的绝佳机会。

1 应用程序类加载器的变化

在所有Java版本中,应用程序类加载器(通常称为系统类加载器)是JVM用于运行应用程序的三个类加载器之一。它加载不需要任何特殊权限的JDK类以及所有应用程序类(除非应用程序使用自己的类加载器,在这种情况下,本节所述内容都不适用)。

可以调用ClassLoader.getSystemClassLoader()或者在某个类的实例上调用getClass().getClassLoader()方法,访问应用程序类加载器。这两种方法都承诺返回ClassLoader类型的实例。

Java 8及之前的版本中,应用程序类加载器是URLClassLoader类型,它是ClassLoader的一个子类型。因为URLClassLoader提供了一些方便的方法,所以人们通常将实例转化为这个类。可以参考代码清单6-1中的例子。

代码清单6-1 将应用程序类加载器转化为URLClassLoader

如果没有模块作为JAR的运行时呈现,URLClassLoader就无法知道要从哪个工件中找到指定的类。因此,一旦需要加载某个类,URLClassLoader就会扫描类路径上的每个工件,直到找到目标为止(如图6-1所示)。这显然非常低效。

没有模块时(上),需扫描类路径上的所有工件以加载指定的类。有了模块后(下),类加载器知道包来自哪个模块化JAR并直接从那里加载它

现在转向Java 9及以上版本。由于有了JAR在运行时的正确呈现,类加载的行为得到了改进:当需要加载某个类时,会标识它所属的包,用于确定从哪个特定的模块化JAR中加载。于是只需要扫描该JAR就可以找到所需的类(如图6-1所示)。

这基于一条假设:没有两个模块化JAR在同名包中含有相同的类型。如果该假设不成立,就会出现包分裂问题,这在模块系统下会引发错误。

新的类型AppClassLoader及其等同的新超类BuiltinClassLoader实现了新的行为。从Java 9开始,应用程序类加载器变成了AppClassLoader

这意味着已很少使用的(URLClassLoader)getClass().getClassLoader()语句将不能再继续工作。如果想了解Java 9及以上版本中关于类加载器的结构和关系的更多信息,可以参见后文。

2 不再通过URLClassLoader来获得类加载器

如果你在依赖的项目中遇到URLClassLoader的类型转换,并且该项目无法更新到Java 9及以上的兼容版本,那么只能采取以下方法之一进行应对。

1)向该项目报告问题,或者提交修复代码。

2)在本地自行建立项目克隆或项目补丁。

3) 等待。

如果该问题无法得到解决,你或许可以切换到另一个支持Java 9及以上版本的库或框架。

如果是自己的代码进行了这样的类型转换,你可以(也必须)采取一些措施。遗憾的是,你可能不得不放弃一两个功能,因为在将类型转换为URLClassLoader时,代码可能使用了它的一些特定API。

尽管ClassLoader中已经添加了一些API,但它尚无法完全取代URLClassLoader。不过,请先静观其变,它的效果也许能满足你的要求。

如果你只需要查看应用程序启动的类路径,那么请检查系统属性java.class.path。如果你已经使用URLClassLoader通过在类路径中指定JAR的方式来动态加载用户提供的代码(例如,作为插件基础结构的一部分),那么你必须找到一种新方法来完成这样的操作,因为使用Java 9及以上版本中的应用程序类加载器是无法完成这一任务的。

相反,考虑创建一个新的类加载器。它具有额外的优势,你将得以摆脱那些新的类,因为它们没有被加载到应用程序类加载器中。如果你至少需要基于Java 9进行编译,那么“层”可能是更好的解决方案。

你或许很想了解AppClassLoader,以探寻是否能够利用它的功能来满足需要。总的来说,这是不需要的!依赖于AppClassLoader会显得非常丑陋,因为它是一个私有的内部类,所以不得不使用反射来调用它。

同时,本文也不推荐依赖其公有的超类BuiltinClassLoader。正如包名jdk.internal.loader所暗示的那样,它是一个内部API。而且由于该软件包是Java 9新增的,在默认情况下不可用,因此必须借助--add-exports甚至--add-opens才能使用它。

但是这样不仅会使代码和构建过程变得复杂,而且在未来的Java更新中还可能导致兼容性问题(比如,当这些类被重构时)。所以,除非绝对有必要实现关键任务功能,否则请不要这样做。

3 寻找制造麻烦的强制类型转换

检查这些强制类型转换的代码很简单:对(URLClassLoader)进行全文搜索即可(此处括号用于查找是否为强制类型转换)。该方法几乎没有误报。至于在依赖项中执行查找,目前还没有找到合适的工具。

将特殊的构建工具(在同一个地方获取所有依赖项的源JAR)、特殊的命令行工具(访问所有的.java文件及其文件内容)和全文搜索相结合,也许可以做到这一点。

三 更新后的运行时镜像目录布局

在20多年的时间里,JDKJRE的目录结构逐渐发展,该过程中虽然产生了一些瑕疵,但这不足为奇。当然,不重新组织它们的一个原因在于向后兼容性。对于每一个细节而言,一些代码的确取决于具体布局,如以下两个例子所示。

1) 某些工具,特别是IDE,依赖于rt.jar(构成核心Java运行时的类)、tools.jar(工具和实用程序的支持类)和src.zipJDK源代码)的准确位置。

2) 有一些代码通过推测正在运行的JRE带有一个同级目录bin,而在里面搜索javac、jarjavadocJava命令,但是只有当JRE是JDK安装中的一部分时,这么做才正确,因为包含这些命令的bin目录在这种情况下与jre目录是相邻的。

但是模块系统的出现打破了这两个例子的基本假设。

1) JDK代码现在已经模块化,因此它应该由单个模块,而不是像rt.jar和tools.jar这样的JAR提供。

2) 借助模块化的Java代码库和jlink等工具,可以将任何模块集创建为运行时镜像。

Java 11开始,不再有独立的JRE包,因此运行程序需要JDK或者由jlink创建的包。

很明显,模块系统会引起一系列重大改变。于是为了一以贯之,需要彻底重新组织运行时镜像目录结构。图6-2展示了相应的结果。总体而言,新的布局更为简单。

1) 单个bin目录,并且没有重复的二进制可执行文件。

2) 单个lib目录。

3) 单个conf目录,包含用于配置的所有文件。

JDK 8JDK 9的目录结构比较,新版本的目录更加清晰

这些改变带来的最直接影响是,你需要更新开发工具,因为旧版本可能不适用于JDK 9及以上版本。在这种情况下,根据项目的不同,对于在JDK/JRE目录中检索二进制文件、属性文件等的代码来说,搜索新版本并进行升级可能非常有意义。

用于获取系统资源的URL(例如ClassLoader::getSystemResource)也已经发生变化。它曾经是以下形式:

其中,${path}是类似于java/lang/String.class的内容。它现在变成了:

创建或使用这类URL的所有JDK接口都在新模式上运行,但是对于手动处理这些URL的非JDK代码而言,需要进行更新以支持Java 9及以上版本。

此外,Class::getResource*ClassLoader::getResource*方法不再读取JDK内部资源。相反,要访问模块的内部资源,请使用Module::getResourceAsStream或创建一个JRT文件系统,如下所示。

关于如何访问资源的更多信息,可以参见后文。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表