Поделиться через


Переход с Java 8 на Java 11

Для перехода с Java 8 на Java 11 не существует универсального решения. Переход нетривиального приложения с Java 8 на Java 11 может быть трудоемким процессом. К потенциальным проблемам относятся удаленные API, устаревшие пакеты, использование внутреннего API, изменения в загрузчиках классов, а также изменения в сборке мусора.

В целом, подходы заключаются в попытке запуска на Java 11 без перекомпиляции, или с начальной компиляцией с JDK 11. Если цель состоит в том, чтобы запустить приложение как можно быстрее, то зачастую лучшим подходом является попытка запустить приложение на Java 11. Для библиотеки целью будет публикация артефакта, который компилируется и тестируется с помощью JDK 11.

Переход на Java 11 трудозатратный. После выхода Java 8 было добавлено много новых функций и улучшений. Эти функции и усовершенствования улучшают запуск, производительность, использование памяти и обеспечивают лучшую интеграцию с контейнерами. Кроме того в API добавлены дополнения и изменения, повышающие производительность разработчиков.

Этот документ касается средств для проверки кода. Здесь также рассматриваются проблемы, с которыми вы можете столкнуться, и рекомендации по их устранению. Вам также следует принять во внимание другие руководства, такие как Oracle JDK Migration Guide (Руководство по миграции Oracle JDK). В этом руководстве не обсуждается вопрос изменения существующего кода на модулярный.

Панель элементов

В Java 11 есть два инструмента, jdeprscan и jdeps, полезные при выявлении потенциальных проблем. Эти инструменты можно запустить в существующих классах или JAR-файлах. Вы можете оценить усилия для перехода, не нуждаясь в выполнении перекомпиляции.

jdeprscan ищет варианты использования нерекомендуемого или удаленного API. Использование нерекомендуемого API не является блокирующей проблемой, однако на него следует обратить внимание. У вас есть обновленный JAR-файл? Нужно ли заносить проблему в журнал, чтобы решить проблему использования нерекомендуемого API? Использование удаленного API — это блокирующая проблема, которую необходимо решить до попытки запуска на Java 11.

jdeps, представляющий собой анализатор зависимости от класса Java. При использовании с опцией --jdk-internals, jdeps сообщает, от которого внутреннего API зависит каждый класс. Вы можете продолжать использовать внутренний API на Java 11, однако приоритетом должна стать замена потребления. На вики-странице OpenJDK Java Dependency Analysis Tool (Средство анализа зависимости от класса Java) содержатся рекомендуемые замены для некоторых широко используемых внутренних API для JDK.

Для Gradle и Maven есть подключаемые модули jdeps и jdeprscan. Мы рекомендуем добавить эти инструменты в ваши скрипты сборки.

Инструмент Подключаемый модуль Gradle Подключаемый модуль Maven
jdeps jdeps-gradle-plugin Подключаемый модуль Apache Maven JDeps
jdeprscan jdeprscan-gradle-plugin Подключаемый модуль Apache Maven JDeprScan

Сам компилятор Java, javac, является еще одним инструментом в вашем инструментарии. Предупреждения и ошибки, которые вы получите от jdeprscan и jdeps, будут выдаваться компилятором. Преимущество использования jdeprscan и jdeps заключается в том, что вы можете запускать эти инструменты поверх существующих файлов JAR и файлов классов, включая сторонние библиотеки.

Что jdeprscan и jdeps не могут сделать, так это предупредить об использовании отражения для доступа к инкапсулированному API. Доступ с помощью рефлексии проверяется во время выполнения. Безусловно, чтобы быть уверенным, код следует запустить на Java 11.

Использование jdeprscan

Самый простой способ использовать jdeprscan — предоставить ему JAR-файл из существующей сборки. Ему можно также предоставить каталог, например выходной каталог компилятора или имя отдельного класса. Используйте параметр --release 11, чтобы получить наиболее полный список нерекомендуемых API. Если вы хотите определить, какой из нерекомендуемых API следует использовать, снова верните для параметра значение --release 8. API, признанный нерекомендуемым на Java 8, скорее всего, будет удален раньше, чем API, признанный нерекомендуемым совсем недавно.

jdeprscan --release 11 my-application.jar

Инструмент jdeprscan генерирует сообщение об ошибке при возникновении проблем с разрешением зависимого класса. Например, error: cannot find class org/apache/logging/log4j/Logger. Рекомендуется добавлять зависимые классы в --class-path или использовать путь класса приложения, однако инструмент продолжит сканирование без него. Аргумент — --class-path. Никакие другие вариации аргумента пути класса не сработают.

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

Этот вывод говорит о том, что класс com.company.Util вызывает нерекомендуемый конструктор класса java.lang.Double. javadoc порекомендует, какой API следует использовать вместо нерекомендуемого. Никакой объем работы не решит error: cannot find class sun/misc/BASE64Encoder, поскольку это API, который был удален. Начиная с Java 8, следует использовать java.util.Base64.

Запустите jdeprscan --release 11 --list, чтобы получить представление о том, какие API стали нерекомендуемыми после Java 8. Чтобы получить список удаленных API, запустите jdeprscan --release 11 --list --for-removal.

Использование jdeps

Используйте jdeps, с опцией --jdk-internals, что найти зависимости от внутреннего API для JDK. Для этого примера нужен параметр командной строки --multi-release 11, поскольку log4j-core-2.13.0.jar является JAR-файлом с несколькими выпусками. Без этой опции при нахождении JAR-файла с несколькими выпусками jdeps будет создавать предупреждение. Параметр определяет, какую версию файлов классов необходимо проверить.

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

Выход дает несколько хороших советов по устранению использования внутреннего API JDK. Замена API предлагается на всех возможных местах. В скобках указано имя модуля, в котором инкапсулирован пакет. Имя модуля можно использовать с --add-exports или --add-opens, если необходимо явно прервать инкапсуляцию.

Использование sun.misc.BASE64Encoder или sun.misc.BASE64Decoder приведет на Java 11 к ошибке java.lang.NoClassDefFoundError. Использующий эти API код следует модифицировать для использования java.util.Base64.

Попробуйте исключить использование любых API, исходящих из модуля jdk.unsupported. API из этого модуля будет ссылаться на Предложение по усовершенствованию JDK (JEP) 260 в качестве предлагаемой замены. Если кратко, то JEP 260 говорит, что использование внутреннего API будет поддерживаться до тех пор, пока не будет доступен заменяющий API. Несмотря на то, что ваш код может использовать внутренний API для JDK, он будет продолжать работать, по крайней мере, некоторое время. Взгляните на JEP 260, так как он указывает на замену некоторых внутренних API. переменные дескрипторы можно использовать, например, вместо некоторых API sun.misc.Unsafe.

jdeps может сделать нечто большее, чем просто проверить использование внутренних компонентов JDK. Это полезный инструмент для анализа зависимостей и создания файлов сведений о модуле. Дополнительные сведения можно найти в документации.

Использование javac

Компиляция с JDK 11 потребует обновлений для сборки скриптов, инструментов, тестовых платформ и включенных библиотек. Используйте параметр -Xlint:unchecked для javac, чтобы получить подробные сведения об использовании внутреннего API JDK и других предупреждений. Также для демонстрации компилятору инкапсулированных пакетов может потребоваться использование --add-opens или --add-reads (см. JEP 261).

Библиотеки могут рассматривать упаковку как JAR-файл с несколькими выпусками. JAR-файлы с несколькими выпусками позволяют поддерживать среды выполнения как Java 8, так и Java 11 из одного и того же JAR-файла. Они действительно добавляют сложность в выполнение сборки. Сведения о сборке JAR-файлов с несколькими выпусками не входят в область, рассматриваемую эти документом.

Выполнение на Java 11

Большинство приложений должно работать на Java 11 без необходимости в изменениях. Первое, что нужно попробовать — это запустить Java 11 без перекомпиляции кода. Смысл простого запуска заключается в том, чтобы посмотреть, какие предупреждения и ошибки будут выданы во время выполнения. При таком подходе
приложение работает на Java 11 быстрее, сосредоточившись на минимуме, который необходимо сделать.

Большинство проблем, с которыми вы можете столкнуться, можно решить, не выполняя перекомпиляцию кода. Если проблему следует исправлять в коде, исправьте ее, но продолжайте компилировать с помощью JDK 8. По возможности работайте над доведением приложения до запуска с javaверсией 11 до компиляции с JDK 11.

Проверка параметров командной строки

Перед выполнением на Java 11 следует выполнить быструю проверку параметров командной строки. Удаленные опции приведут к выходу виртуальной машины Java. Эта проверка особенно важна, если вы используете опции ведения журнала для сборки мусора, поскольку они радикально изменились с Java 8. Инструмент JaCoLine хорошо подходит для обнаружения проблем с параметрами командной строки.

Проверка сторонних библиотек

Потенциальным источником проблем являются сторонние библиотеки, которые вы не контролируете. Вы можете заблаговременно обновлять сторонние библиотеки до более новых версий. Или посмотреть, что выходить за пределы выполнения приложения и обновить только нужные библиотеки. Проблема с обновлением всех библиотек до последней версии заключается в том, что это затрудняет поиск первопричины появления в приложении ошибки. Ошибка произошла из-за какой-то обновленной библиотеки? Или ошибка была вызвана каким-то изменением в среде выполнения? Проблема обновления связана только с тем, что для ее решения может потребоваться несколько итераций.

Рекомендуется вносить как можно меньше изменений и обновлять сторонние библиотеки в виде отдельных трудозатрат. Если вы обновляете стороннюю библиотеку, то чаще всего вам потребуется самая последняя версия, совместимая с Java 11. В зависимости от того, насколько отстает ваша текущая версия, возможно, вы захотите применить более осторожный подход и обновить ее до первой версии, совместимой с Java 9+.

Кроме просмотра заметок о выпуске, для оценки JAR-файла можно использовать jdeps и jdeprscan. Кроме того, Группа контроля качества OpenJDK поддерживает вики-страницу Quality Outreach (Популяризация качественного содержимого), на которой перечислен статус тестирования многих проектов FOSS с версиями OpenJDK.

Явно установленная сборка мусора

Сборщик мусора Parallel (Parallel GC) — это стандартный сборщик мусора в Java 8. Если приложение использует значение по умолчанию, сборку мусора следует явно задать с помощью параметра командной строки -XX:+UseParallelGC. Вместо значения по умолчанию в Java 9 используется сборщик мусора G1GC (Garbage First Garbage Collector). Чтобы справедливо сравнить приложение, работающее на Java 8, с Java 11, настройки сборки мусора должны быть одинаковыми. Экспериментирование с настройками сборки мусора следует отложить, пока приложение не будет проверено на Java 11.

Явно заданные параметры по умолчанию

При запуске на ВМ HotSpot, установка параметра командной строки -XX:+PrintCommandLineFlags будет сбрасывать значения параметров, установленных ВМ, в частности, значения по умолчанию, установленные сборкой мусора. Запускайте с этим флагом на Java 8 и используйте печатные параметры при запуске на Java 11. По большей части, значения по умолчанию для версий от 8 до 11 одинаковы. Но использование настроек из 8 обеспечивает четность.

Рекомендуется использовать параметр командной строки --illegal-access=warn. Использование рефлексии для доступа к внутреннему API для JDK в Java 11 приведет к появлению предупреждения о недопустимом доступе с помощью рефлексии. По умолчанию предупреждение выдается только при первом недопустимом доступе. Установка --illegal-access=warn приведет к вызову предупреждения при каждом недопустимом доступе с помощью рефлексии. Установив параметр на предупреждение, вы сможете найти еще больше случаев недопустимого доступа. Но вы также будете получать много лишних предупреждений.
После запуска приложения на Java 11, установите --illegal-access=deny для имитации будущего поведения во время выполнения Java. Начиная с Java 16, значением по умолчанию будет --illegal-access=deny.

Предостережения ClassLoader

В Java 8 загрузчик системного класса можно привести к URLClassLoader. Обычно это выполняется приложениями и библиотеками, которые хотят внедрить классы в путь к классу во время выполнения. В Java 11 иерархия классов загрузчиков изменилась. Загрузчик системного класса (также известный как загрузчик класса приложения) теперь является внутренним классом. Приведение к URLClassLoader вызовет ClassCastException во время выполнения. Java 11 не имеет API для динамического дополнения пути к классу во время выполнения, но это можно сделать с помощью рефлексии, с очевидными предостережениями об использовании внутреннего API.

В Java 11 загрузчик класса загрузки загружает только основные модули. При создании загрузчика классов с нулевым родительским элементом, он сможет найти не все классы платформы. В Java 11 в таких случаях в качестве загрузчика класса родительского элемента нужно передавать ClassLoader.getPlatformClassLoader(), а не null.

Изменения локальных данных

Источник локальных данных по умолчанию на Java 11 изменен с помощью JEP 252 на Единый репозиторий локальных данных консорциума Юникод. Это может повлиять на локальное форматирование. При необходимости установите системное свойство java.locale.providers=COMPAT,SPI для возврата к поведению языкового стандарта Java 8.

Возможные проблемы

Вот некоторые из общих проблем, с которыми вы можете столкнуться. Более подробные сведения по этим вопросам см. по ссылкам.

Нераспознанные параметры

Если параметр командной строки удален, то приложение выведет Unrecognized option: или Unrecognized VM option, за которым последует имя конфликтующего параметра. Нераспознанный параметр приведет к выходу ВМ. Нерекомендуемые параметры, которые не были удалены, будут выдавать предупреждение ВМ.

В общем, удаленные параметры не заменяются, и единственным решением является удаление параметра из командной строки. Исключением являются опции для ведения журнала о сборе мусора. Ведение журнала о сборке мусора реализовано в Java 9 для использования унифицированной платформы ведения журналов JVM. См. "Table 2-2 Mapping Legacy Garbage Collection Logging Flags to the Xlog Configuration" (Таблица 2-2. Сопоставление флагов ведения журнала устаревших сборок мусора с конфигурацией Xlog) в разделе Enable Logging with the JVM Unified Logging Framework (Включение ведения журнала с помощью унифицированной платформы ведения журналов JVM) из Справочника по инструментам Java SE 11.

Предупреждения ВМ

Использование нерекомендуемых параметров приведет к созданию предупреждения. Параметр становится нерекомендуемым, если он заменен или больше не используется. Как и в случае с удаленными параметрами, эти параметры следует удалить из командной строки. Предупреждение VM Warning: Option <option> was deprecated означает, что параметр все еще поддерживается, но эта поддержка может быть удалена в будущем. Параметр, который больше не поддерживается, будет выдавать предупреждение VM Warning: Ignoring option. Неподдерживаемые параметры не влияют на среду выполнения.

На веб-странице VM Options Explorer (Обозреватель параметров ВМ) представлен исчерпывающий список параметров, которые были добавлены в Java, начиная с JDK 7 или добавлены в него.

Ошибка: Не удалось создать виртуальную машину Java.

Это сообщение об ошибке выводится, когда виртуальная машина Java сталкивается с нераспознанным параметром.

ПРЕДУПРЕЖДЕНИЕ. Возникла недопустимая операция доступа с помощью рефлексии

Если Java-код использует отражение для доступа к внутреннему API JDK, во время выполнения будет выдано предупреждение о недопустимом доступе с помощью рефлексии.

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Это означает, что модуль не экспортировал пакет, доступ к которому осуществляется через отражение. Пакет инкапсулирован в модуль и по сути является внутренним API. Предупреждение можно проигнорировать в качестве первого шага по установке и запуску Java 11. Время выполнения Java 11 разрешает доступ с помощью рефлексии, так что старый код может продолжать работу.

Чтобы решить это предупреждение, найдите обновленный код, который не использует внутренний API. Если проблему невозможно решить с помощью обновленного кода, для открытия доступа к пакету можно использовать либо параметр --add-exports, либо параметр командной строки --add-opens. Эти параметры позволяют получить доступ к неэкспортированным типам одного модуля из другого.

Параметр --add-exports позволяет целевому модулю получить доступ к общедоступным типам именованного пакета модуля-источника. Иногда для доступа к членам и API, не являющимся открытыми, код будет использовать setAccessible(true). Он известен, как глубокое отражение. В этом случае используйте --add-opens для предоставления вашему коду доступа к членам пакета, которые не являются открытыми. Если вы не уверены, использовать --add-exports или --add-opens, начните с --add-exports.

Параметры --add-exports или --add-opens следует рассматривать как обходной путь, а не как долгосрочное решение. Использование этих опций прерывает инкапсуляцию модульной системы, которая должна препятствовать использованию внутреннего API JDK. Если внутренний API будет удален или изменен, приложение выйдет из строя. Доступ с помощью рефлексии будет запрещен на Java 16, за исключением случаев, когда доступ будет разрешен параметрами командной строки, такими как --add-opens. Чтобы имитировать будущее поведение, установите --illegal-access=deny в командной строке.

Предупреждение в приведенном выше примере выдается потому, что пакет sun.nio.ch не экспортируется модулем java.base. Другими словами, в файле module-info.java модуля java.base нет exports sun.nio.ch;. Это можно решить с помощью --add-exports=java.base/sun.nio.ch=ALL-UNNAMED. Классы, не определенные в модуле, неявно принадлежат неименованному модулю, буквально названному ALL-UNNAMED.

java.lang.reflect.InaccessibleObjectException

Это исключение указывает на то, что вы пытаетесь вызвать setAccessible(true) в поле, или метод инкапсулированного класса. Вы также можете получить предупреждение о недопустимом доступе с помощью рефлексии. Используйте --add-opens для предоставления вашему коду доступа к членам пакета, которые не являются открытыми. Сообщение об исключении сообщит, что модуль "не открывает" пакет модулю, который пытается вызвать setAccessible. Если модуль является "неименованным", используйте UNNAMED-MODULE в качестве целевого модуля в параметре --add-opens.

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

Ошибка NoClassDefFoundError, скорее всего, вызвана разделенным пакетом или ссылкой на удаленные модули.

Ошибка NoClassDefFoundError, вызванная разделением пакетов

Раздельный пакет —это пакет, находящийся в нескольких библиотеках. Симптомом проблемы разделения пакетов является то, что класс, который должен находится в пути класса, невозможно найти.

Эта проблема будет возникать только при использовании пути модуля. Система модулей Java оптимизирует поиск классов, ограничивая пакет одним именованным модулем. При выполнении поиска классов предпочтение отдается пути модуля, а не класса. Если пакет разделен между модулем и путем к классу, то для поиска класса используется только модуль. Это может привести к ошибкам NoClassDefFound.

Простой способ проверки разделенного пакета — подключить путь к модулю и к классу в jdeps и использовать путь к файлам класса приложения в качестве значения <path>. Если есть разделенный пакет, jdeps выведет предупреждение: Warning: split package: <package-name> <module-path> <split-path>.

Эту проблему можно решить, используя --patch-module <module-name>=<path>[,<path>] для добавления разделенного пакета в именованный модуль.

Ошибка NoClassDefFoundError, вызванная использованием модулей Java EE или CORBA

Если приложение работает на Java 8, но вызывает java.lang.NoClassDefFoundError или java.lang.ClassNotFoundException, то вполне вероятно, что приложение использует пакет из модулей Java EE или CORBA. Эти модули признаны нерекомендуемыми на Java 9 и удалены на Java 11.

Чтобы решить проблему, добавьте в проект зависимость среды выполнения.

Удаленный модуль Затронутый пакет Предлагаемая зависимость
Java API для Веб-служб XML (JAX-WS) java.xml.ws Среда выполнения RI для JAX WS
Архитектура Java для привязки XML (JAXB) java.xml.bind Среда выполнения JAXB
Платформа активации JavaBeans (JAV) java.activation Платформа активации JavaBeans (TM)
Общие заметки java.xml.ws.annotation API заметки Javax
Архитектура брокера запросов общих объектов (CORBA) java.corba GlassFish CORBA ORB
API транзакций Java (JTA) java.transaction API транзакций Java

-Xbootclasspath/p больше не поддерживается

Поддержка для -Xbootclasspath/p удалена. Используйте вместо этого --patch-module. Параметр --patch-module описан в JEP 261. Ищите раздел с названием "Patching module content" (Исправление содержимого модуля). --patch-module можно использовать с javac и java для переопределения или дополнения классов в модуле.

Что делает --patch-module, так это вставляет модуль исправления в поиск класса модульной системы. Модульная система сначала захватит класс из модуля исправления. Это тот же эффект, что и в случае с bootclasspath на Java 8.

UnsupportedClassVersionError

Это исключение означает, что вы пытаетесь запустить код, который был скомпилирован с более поздней версией Java, на более ранней версии Java. Например, вы работаете на Java 11 с JAR-файлом, скомпилированным с JDK 13.

Версия Java Версия формата файла класса
8 52
9 53
10 54
11 55
12 56
13 57

Дальнейшие действия

После того, как приложение запустится на Java 11, рассмотрите возможность переноса библиотек с пути класса на путь модуля. Выполните поиск обновленных версий библиотек, от которых зависит ваше приложение. Если возможно, выбирайте модульные библиотеки. Используйте путь модулей, даже если вы не планируете использовать модули в своем приложении. Использование пути модуля предоставляет производительность при загрузке классов, которая намного лучше производительности при использовании пути модуля.