1. 概述

Java 平台在 Java 9 之前采用的是单体式架构,所有类库打包为一个整体。

从 Java 9 开始,引入了 Java 平台模块系统(JPMS,Java Platform Module System),简称模块系统。模块成为代码组织和复用的基本单位,模块取代了包成为复用的基本单元

本文将介绍在将旧有 Java 应用迁移到 Java 9 时可能遇到的一些典型问题,并提供相应的解决方案。


2. 简单示例

我们来看一个 Java 8 的简单应用,它包含四个方法,这些方法在 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);
}

第三个方法使用 JAXB 将 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);
}

第四个方法使用 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.com.baeldung.prejpms.App
3.com.baeldung.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.com.baeldung.prejpms.App
3.com.baeldung.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
   com.baeldung.prejpms            -> com.sun.crypto.provider               JDK internal API (java.base)
   com.baeldung.prejpms            -> java.io                               java.base
   com.baeldung.prejpms            -> java.lang                             java.base
   com.baeldung.prejpms            -> javax.xml.bind                        java.xml.bind
   com.baeldung.prejpms            -> javax.xml.bind.annotation             java.xml.bind
   com.baeldung.prejpms            -> org.slf4j                             not found
   com.baeldung.prejpms            -> sun.misc                              JDK internal API (JDK removed internal API)
   com.baeldung.prejpms            -> sun.reflect                           JDK internal API (jdk.unsupported)

输出中包含三列信息:

  • 应用中的包名
  • 依赖的类库
  • 所属模块或是否为 JDK 内部 API

4. 被弃用的模块

首先看第一个错误:java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext

根据 jdeps 输出,javax.xml.bind 属于 java.xml.bind 模块,该模块在 Java 9 中 被标记为弃用(deprecated),意味着它不会默认加载。

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>
...

短期解决方案:使用 --add-modules 加载模块。

不推荐长期使用:因为该模块未来可能被彻底移除。

推荐长期解决方案:使用 Maven 显式引入 JAXB 依赖:

<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,Java 9 中已将其标记为不可用。

使用 jdeps--jdk-internals 参数查看建议的替代方案:

>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 官方支持的 API。

例如,将 BASE64Encoder 替换为:

String encodedString = Base64.getEncoder().encodeToString(inputString.getBytes());

⚠️ 注意:

  • sun.reflect.Reflection 被移到了 jdk.unsupported 模块,Java 9 默认加载,所以不会报错。
  • com.sun.crypto.provider.SunJCE 是特定于某些 JDK 实现的,只要运行在相同实现上就不会出错。

但总体建议:避免使用内部 API,改用标准 API,以提高兼容性和可维护性。


6. 总结

本文介绍了 Java 9 引入模块系统后可能导致的迁移问题,主要包括:

✅ 被弃用模块的加载问题(如 JAXB)
✅ 使用内部 API(如 sun.misc)导致的类找不到问题

解决方案包括:

问题类型 短期方案 长期方案
被弃用模块 使用 --add-modules 显式加载 添加第三方依赖(如 Maven)
内部 API 检查是否默认可用(如 jdk.unsupported) 替换为标准 API(如 java.util.Base64

如需完整代码示例,请访问 GitHub


» 下一篇: Groovy 中的 Map 操作