Java 9迁移问题和解决方案

2023/07/05

1. 概述

Java平台曾经有一个整体式架构,将所有包捆绑为一个单元。

在Java 9中,随着Java平台模块系统(JPMS)或简称模块的引入,相关的包被归类到模块下,模块取代包成为复用的基本单位

在这个简短的教程中,我们将介绍在将现有应用程序迁移到Java 9时可能会遇到的一些与模块相关的问题。

2. 简单示例

让我们来看一个简单的Java 8应用程序,它包含4个方法,这4个方法在Java 8下有效,但在未来的版本中具有挑战性。我们将使用这些方法来了解迁移到Java 9的影响。

第一个方法获取应用程序中引用的JCE提供者的名称

private static void getCrytpographyProviderName() {
    LOGGER.info("1. JCE Provider Name: {}\n", new SunJCE().getName());
}

第二个方法列出堆栈跟踪中类的名称

private static void getCallStackClassNames() {
    StringBuffer sbStack = new StringBuffer();
    int i = 0;
    Class<?> caller = Reflection.getCallerClass(i++);
    do {
        sbStack.append(i + ".").append(caller.getName())
            .append("\n");
        caller = Reflection.getCallerClass(i++);
    } while (caller != null);
    LOGGER.info("2. Call Stack:\n{}", sbStack);
}

第三个方法将Java对象转换为XML

private static void getXmlFromObject(Book book) throws JAXBException {
    Marshaller marshallerObj = JAXBContext.newInstance(Book.class).createMarshaller();
    marshallerObj.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

    StringWriter sw = new StringWriter();
    marshallerObj.marshal(book, sw);
    LOGGER.info("3. Xml for Book object:\n{}", sw);
}

最后一个方法使用来自JDK内部库的sun.misc.BASE64Encoder将字符串编码为Base64

private static void getBase64EncodedString(String inputString) {
    String encodedString = new BASE64Encoder().encode(inputString.getBytes());
    LOGGER.info("4. Base Encoded String: {}", encodedString);
}

让我们从main方法调用所有方法:

public static void main(String[] args) throws Exception {
    getCrytpographyProviderName();
    getCallStackClassNames();
    getXmlFromObject(new Book(100, "Java Modules Architecture"));
    getBase64EncodedString("Java");
}

当我们在Java 8中运行这个应用程序时,我们得到以下信息:

> java -jar target\pre-jpms.jar
[INFO] 1. JCE Provider Name: SunJCE

[INFO] 2. Call Stack:
1.sun.reflect.Reflection
2.cn.tuyucheng.taketoday.prejpms.App
3.cn.tuyucheng.taketoday.prejpms.App

[INFO] 3. Xml for Book object:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book id="100">
    <title>Java Modules Architecture</title>
</book>

[INFO] 4. Base Encoded String: SmF2YQ==

通常,Java版本保证向后兼容性,但JPMS改变了其中的一些。

3. 在Java 9中执行

现在,让我们在Java 9中运行这个应用程序:

>java -jar target\pre-jpms.jar
[INFO] 1. JCE Provider Name: SunJCE

[INFO] 2. Call Stack:
1.sun.reflect.Reflection
2.cn.tuyucheng.taketoday.prejpms.App
3.cn.tuyucheng.taketoday.prejpms.App

[ERROR] java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext
[ERROR] java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder

我们可以看到前两个方法运行良好,而后两个方法失败了。让我们通过分析应用程序的依赖关系来调查失败的原因。我们将使用Java 9附带的jdeps工具:

>jdeps target\pre-jpms.jar
   cn.tuyucheng.taketoday.prejpms            -> com.sun.crypto.provider               JDK internal API (java.base)
   cn.tuyucheng.taketoday.prejpms            -> java.io                               java.base
   cn.tuyucheng.taketoday.prejpms            -> java.lang                             java.base
   cn.tuyucheng.taketoday.prejpms            -> javax.xml.bind                        java.xml.bind
   cn.tuyucheng.taketoday.prejpms            -> javax.xml.bind.annotation             java.xml.bind
   cn.tuyucheng.taketoday.prejpms            -> org.slf4j                             not found
   cn.tuyucheng.taketoday.prejpms            -> sun.misc                              JDK internal API (JDK removed internal API)
   cn.tuyucheng.taketoday.prejpms            -> sun.reflect                           JDK internal API (jdk.unsupported)

该命令的输出给出:

  • 第一列包含我们应用程序中所有包的列表
  • 第二列包含我们应用程序中所有依赖项的列表
  • 依赖项在Java 9平台中的位置-这可以是模块名称,或内部JDK API,或者第三方库没有对应的模块

4. 弃用的模块

现在让我们尝试解决第一个错误java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext。

根据依赖项列表,我们知道java.xml.bind包属于java.xml.bind模块,这似乎是一个有效的模块。那么,让我们看看这个模块的官方文档吧。

官方文档说java.xml.bind模块已弃用,将在未来的版本中删除。因此,默认情况下该模块不会加载到类路径中。

但是,Java提供了一种通过使用–add-modules选项按需加载模块的方法。所以,让我们继续尝试一下:

>java --add-modules java.xml.bind -jar target\pre-jpms.jar
...
INFO 3. Xml for Book object:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<book id="100">
    <title>Java Modules Architecture</title>
</book>
...

我们可以看到执行成功了。此解决方案快速简便,但不是最佳解决方案。

作为长期解决方案,我们应该使用Maven添加依赖项作为第三方库:

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.4.0-b180725.0427</version>
</dependency>

5. JDK内部API

现在让我们看看第二个错误java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder。

从依赖列表可以看出sun.misc包是一个JDK内部API。

顾名思义,内部API就是私有代码,仅供在JDK内部使用。

在我们的示例中,内部API似乎已从JDK中删除。让我们使用–jdk-internals选项来检查替代API是什么:

>jdeps --jdk-internals target\pre-jpms.jar
...
JDK Internal API                         Suggested Replacement
----------------                         ---------------------
com.sun.crypto.provider.SunJCE           Use java.security.Security.getProvider(provider-name) @since 1.3
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.reflect.Reflection                   Use java.lang.StackWalker @since 9

我们可以看到Java 9推荐使用java.util.Base64而不是sun.misc.Base64Encoder。因此,我们的应用程序必须更改代码才能在Java 9中运行。

请注意,我们在应用程序中使用了另外两个内部API,Java平台建议对其进行替换,但我们没有收到任何错误:

  • 一些内部API(如sun.reflect.Reflection)被认为对平台至关重要,因此被添加到特定于JDK的jdk.unsupported模块中。这个模块默认在Java 9的类路径中可用。
  • com.sun.crypto.provider.SunJCE等内部API仅在某些Java实现中提供,只要使用它们的代码在同一个实现上运行,它就不会抛出任何错误。

在此示例中的所有情况下,我们都使用内部API,这不是推荐的做法。因此,长期的解决方案是将它们替换为平台提供的合适的公共API。

6. 总结

在本文中,我们看到了Java 9中引入的模块系统如何导致一些使用已弃用API或内部API的旧应用程序出现迁移问题。

我们还看到了如何对这些错误应用短期和长期修复。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章