Перейти к основному содержимому

Проблемы и решения миграции на Java 9

· 5 мин. чтения

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 .