Java 9 旧API迁移问题
1. 概述
Java 平台曾经有一个整体架构,将所有包捆绑为一个单元。 在 Java 9 中,通过引入Java 平台模块系统 (JPMS) 或简称模块来简化这一过程。相关的包被归类到模块下,模块取代包成为复用的基本单位。
在本快速教程中,我们将讨论在将现有应用程序迁移到 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);
}
第三种方法将 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将字符串编码为 Base 64:
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.blogdemo.prejpms.App
3.com.blogdemo.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.blogdemo.prejpms.App
3.com.blogdemo.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.blogdemo.prejpms -> com.sun.crypto.provider JDK internal API (java.base)
com.blogdemo.prejpms -> java.io java.base
com.blogdemo.prejpms -> java.lang java.base
com.blogdemo.prejpms -> javax.xml.bind java.xml.bind
com.blogdemo.prejpms -> javax.xml.bind.annotation java.xml.bind
com.blogdemo.prejpms -> org.slf4j not found
com.blogdemo.prejpms -> sun.misc JDK internal API (JDK removed internal API)
com.blogdemo.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.3.1</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。