1. Обзор
Раньше платформа Java имела монолитную архитектуру, объединяющую все пакеты в единое целое.
В Java 9 это было упрощено с введением системы Java Platform Module System (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);
}
И последний метод кодирует строку в Base 64 с помощью sun.misc.BASE64Encoder
из внутренних библиотек JDK :
private static void getBase64EncodedString(String inputString) {
String encodedString = new BASE64Encoder().encode(inputString.getBytes());
LOGGER.info("4. Base Encoded String: {}", encodedString);
}
Давайте вызовем все методы из основного метода:
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.foreach.prejpms.App
3.com.foreach.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.foreach.prejpms.App
3.com.foreach.prejpms.App
[ERROR] java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext
[ERROR] java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder
Мы видим, что первые два метода работают нормально, а последние два не работают. Давайте исследуем причину сбоя, проанализировав зависимости нашего приложения . Мы будем использовать инструмент jdeps
, поставляемый с Java 9:
>jdeps target\pre-jpms.jar
com.foreach.prejpms -> com.sun.crypto.provider JDK internal API (java.base)
com.foreach.prejpms -> java.io java.base
com.foreach.prejpms -> java.lang java.base
com.foreach.prejpms -> javax.xml.bind java.xml.bind
com.foreach.prejpms -> javax.xml.bind.annotation java.xml.bind
com.foreach.prejpms -> org.slf4j not found
com.foreach.prejpms -> sun.misc JDK internal API (JDK removed internal API)
com.foreach.prejpms -> sun.reflect JDK internal API (jdk.unsupported)
Вывод команды дает:
- список всех пакетов внутри нашего приложения в первом столбце
- список всех зависимостей внутри нашего приложения во второй колонке
- расположение зависимостей на платформе Java 9 — это может быть имя модуля, внутренний API JDK или отсутствие для сторонних библиотек
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. Внутренние API JDK
Давайте теперь рассмотрим вторую ошибку java.lang.NoClassDefFoundError: sun/misc/BASE64Encoder.
Из списка зависимостей видно, что пакет sun.misc
является внутренним
API JDK.
Внутренние API, как следует из названия, представляют собой закрытый код, используемый внутри JDK.
В нашем примере внутренний API, похоже, был удален из JDK .
Давайте проверим альтернативный API для этого, используя опцию –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 9 рекомендует использовать java.util.Base64
вместо sun.misc.Base64Encoder.
Следовательно, изменение кода является обязательным для нашего приложения для работы в Java 9. ``
Обратите внимание, что есть два других внутренних API, которые мы используем в нашем приложении, для которых платформа Java предложила замену, но для них мы не получили никаких ошибок:
- Некоторые внутренние API, такие как
sun.reflect.Reflection
, считались критически важными для платформы и поэтому были добавлены в модульjdk.unsupported
для JDK . Этот модуль доступен по умолчанию в пути к классам в Java 9. - Внутренние API, такие как
com.sun.crypto.provider.SunJCE
, предоставляются только в определенных реализациях Java. Пока код, использующий их, выполняется в той же реализации, он не выдаст никаких ошибок.
Во всех случаях в этом примере мы используем внутренние API, что не рекомендуется . Поэтому долгосрочное решение состоит в том, чтобы заменить их подходящими общедоступными API, предоставляемыми платформой.
6. Заключение
В этой статье мы увидели, как система модулей, представленная в Java 9, может вызвать проблемы с миграцией для некоторых старых приложений, использующих устаревшие или внутренние API .
Мы также увидели, как применять краткосрочные и долгосрочные исправления для этих ошибок.
Как всегда, примеры из этой статьи доступны на GitHub .