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


Переход с 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. Как сделать существующий модульный код не рассматривается здесь.

Панель инструментов

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

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

jdeps, который является анализатором зависимостей класса Java. При использовании с параметром --jdk-internalsjdeps сообщает, какой класс зависит от внутреннего API. Вы можете продолжать использовать внутренний API в Java 11, но замена использования должна быть приоритетом. На вики-странице OpenJDK средство анализа зависимостей Java рекомендуется заменить некоторые часто используемые внутренние API JDK.

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

Инструмент Плагин 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. Никакие другие варианты аргумента 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 вместо устаревшего 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-файлом с несколькими выпусками. Без этого параметра jdeps будет жаловаться, если он находит jar-файл с несколькими выпусками. Параметр указывает, какую именно версию файлов классов инспектировать.

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.lang.NoClassDefFoundError в Java 11. Код, использующий эти 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. По возможности запустите приложение с версией 11 перед java с JDK 11.

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

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

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

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

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

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

Явно задать сборку мусора

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

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

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

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

Предупреждения загрузчика классов

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

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

Изменения данных региональных настроек

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

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

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

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

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

Как правило, параметры, которые были удалены, не имеют замены, и единственный способ — удалить параметр из командной строки. Исключение — это варианты ведения журнала сборки мусора. Ведение журнала GC было повторно выполнено в Java 9 для использования единой платформы ведения журналов JVM. См. статью "Таблица 2-2. Сопоставление устаревших флагов ведения журналов мусора с конфигурацией Xlog" в разделе "Включение ведения журнала с помощью единой платформы ведения журналов JVM " справочника по средствам Java SE 11.

Предупреждения виртуальной машины

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

Обозреватель параметров виртуальной машины веб-страницы предоставляет полный список параметров, которые были добавлены или удалены из Java с JDK 7.

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

Это сообщение об ошибке выводится при обнаружении нераспознанного параметра JVM.

ПРЕДУПРЕЖДЕНИЕ: произошла незаконная операция с использованием отражений

Если код 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 параметр позволяет целевому модулю получить доступ к общедоступным типам именованного пакета исходного модуля. Иногда код будет использовать setAccessible(true) для доступа к закрытым членам и API. Это называется глубоким отражением. В этом случае используйте --add-opens для предоставления коду доступа к недоступным членам пакета. Если вы не уверены, следует ли использовать --add-exports или --add-opens, начните с --add-exports.

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

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

java.lang.reflect.InaccessibleObjectException

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

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, вызванный разделением пакетов

Пакет считается "разделённым," когда он обнаружен в нескольких библиотеках. Симптом проблемы с разделенным пакетом заключается в том, что класс, который, как вы знаете, присутствует в class-path, не найден.

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

Простой способ проверить наличие разбиения пакета — ввести пути к модулю и классу в jdeps и использовать их для путей к файлам класса вашего приложения. Если есть разделенный пакет, 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.

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

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

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

Поддержка для -Xbootclasspath/p была удалена. Вместо этого используйте --patch-module. Параметр --patch-module описан в JEP 261. Найдите раздел с надписью "Патчинг содержимого модуля". --patch-module можно использовать с javac и с java для переопределения или расширения классов в модуле.

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

НеподдерживаемыйClassVersionError

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

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

Дальнейшие шаги

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