Проблемы при переходе на Java 9
В конце мая я выступал на JEEConf 2017, где рассказывал об эффективности и производительности Java библиотек и фреймворков. Любое упоминание о производительности должно ссылаться на результаты измерений, что я и сделал во время доклада. Но я решил пойти дальше и протестировать один и тот же код на двух версиях — Java 8 и Java 9.
Хоть Java 9 и выходит только в июле (уже в сентябре), но она уже перешла в стадию Release Candidate, то есть никаких новых фич больше не будет и все критические баги должны быть исправлены. Я начал пользоваться Java 8 за 5 месяцев до релиза и никаких особых проблем при этом не испытал. В то же время главная фича новой версии — модуляризация (проект Jigsaw) привела к многочисленным изменениям внутри самой JDK. Хорошо было бы протестировать библиотеки на новой версии. В сети есть много статей и видео-докладов на тему проектирования модулей. Но сначала нужно проверить, а будет ли работать существующее приложение, и если будет, то как отразится миграция на скорости работы нашей системы. Не секрет, что многие компании остаются на старых версиях (Java 5 — 7) частично из-за боязни новых ошибок и проблем в производительности. Если же приложение работать не будет, насколько просто это починить?
Поэтому я призываю всех установить JDK 9 и протестировать свое приложение. Разумеется, вы можете и дальше использовать текущую версию Java, но прогресс не стоит на месте. Через пару лет выйдет Java 10, а с ней возможно и свойства, и value types, и все равно придется использовать модульность. Тем более что современные IDE (IntelliJ IDEA) уже поддерживают все фичи Java 9.
Системы сборки. Maven
Сборка проекта — это процесс, который проходит постоянно в течение рабочего дня как на компьютере разработчика, так и на CI сервере. Поэтому здесь требуется максимальное быстродействие. Для тестирования сборки я выбрал проект, который я разрабатываю в рамках книги «Разработка Java приложений», тем более что он поддерживает и Maven, и Gradle. Проект постоянно обновляется, поэтому версии зависимостей в нем года либо конца года. Вы сами можете скачать проект и протестировать на своей машине.
Поддерживают ли Maven/Gradle Java 9? Это интересный вопрос, тем более что их модель зависимостей чем-то похожа и даже является конкурентом JDK 9.
Итак, я установил JDK Early Access preview build 171 на Windows систему. Результат запуска команды «java -version»:
Начнем с Maven. Для тестирования я выбрал последнюю версию — 3.5.0. Сам Maven запускается без проблем, выдает корректную версию JDK:
Теперь запустим сборку проекта: mvn clean install. Сборка падает на этапе запуска тестов с ошибкой:
Почему-то стандартный класс JAXBException больше не находится. Дело в том, что все, связанное с JAXB, перенесено Java в модуль java.xml.bind, который автоматически не добавляется в modulepath при запуске JVM, потому что не входит в дефолтный модуль java . se. Как выйти из этого положения? Либо явно указать этот модуль для запуска JVM, либо агрегатный модуль java . se . ee, куда входит все, что связано с Java EE API. Разумеется, добавить агрегатный модуль удобнее, чем каждый модуль по отдельности. Поэтому добавляем переменную окружения MAVEN_OPTS (если у вас ее еще нет), и присваиваем ей значение «—add-modules java . se . ee».
Однако ошибка не исчезает. Все дело в том, что тесты запускает Maven Surfire плагин, который стартует новый JVM процесс. Для того, чтобы указать новые модули, нужно добавить Surfire плагин в главный pom.xml и передать параметры там:
Перезапускаем сборку и получаем новое исключение:
Эта проблема не имеет очевидного решения, потому что JTA библиотека (где находится класс SystemException) присутствует в class-path. Поэтому придется все же в Surfire плагине использовать модуль java.xml.bind:
Перезапускаем сборку Maven и получаем новое исключение:
В чем заключается ошибка? Hibernate пытается через reflection сделать публичным метод, который является protected или private. До Java 9 это было небезопасно, но возможно. Сейчас это запрещено, если только модуль, в котором находится метод, не объявлен открытым (open). Однако есть лазейка, помогает обойти это ограничение. Нужно добавить к аргументам JVM строку —add-opens java . base/java . lang=ALL-UNNAMED
Здесь мы указываем название модуля, затем пакет и тот модуль, для которого мы открываем доступ. Так как наш проект не использует модули, то мы указываем константу ALL-UNNAMED, которой открыт доступ для всех анонимных модулей. Анонимные модули — это обычные jar файлы, в которых нет файла конфигурации module-info . class. Они были добавлены, чтобы разработчикам библиотек было легче перейти на Java 9.
Добавляем эту строку к аргументам командной строки для плагина Surfire и перезапускаем сборку Maven. Получаем новое исключение, но уже при запуске других тестов:
Как вы видите из stacktrace, проблема происходит на стадии инициализации JMockit. Что можно сделать? Можно обновить версию Jmockit с 1.30 на 1.32. Но это не помогает. Уже готовится версия JMockit, но она все еще в стадии beta. Анализ исходников JDK позволяет обнаружить системную переменную JVM, которая позволяет присоединиться к текущей JVM из нее самое. Нужно лишь установить этот флаг в true: -Djdk.attach.allowAttachSelf=true. Подробнее можно прочесть здесь
Добавляем флаг к параметрам в Surefire плагине и перезапускаем сборку Maven. Получаем новую ошибку:
Подробнее об этой проблеме можно прочесть здесь.
А лечится она обновлением версии war-плагина с 2.6 на 3.1.0:
Меняем версию и перезапускаем сборку. Получаем новую ошибку:
С похожей проблемой мы уже сталкивались. Нужно лишь «открыть» доступ к пакетам, добавив параметры в MAVEN_OPTS:
Перезапускаем сборку Maven и получаем новую ошибку:
Stacktrace говорит об ошибке более подробно:
Проблема в том, что такого класса больше нет в JDK. Можно вручную добавить необходимые классы, как сделали здесь.
А можно обновить версию Lombok (c 1.16.14 до 1.16.16):
Перезапускаем сборку Maven и получаем новое исключение при запуске тестов:
После небольшого исследования выяснилось, что проблема исходит от библиотеки тестирования JGlue, однако его последняя версия была выпущена в июле 2016 года и неизвестно, насколько быстро выйдет новая версия.
На этом мое тестирование Maven завершилось. Часть проблем удалось решить, но в целом, их довольно много как для одного проекта. Проект собирается успешно, но часть тестов при этом не проходит.
Системы сборки. Gradle
Теперь проверим, насколько совместим Gradle с JDK 9. Возьмем последнюю версию первого — 3.5.0, обновим Lombok зависимость и запустим сборку: gradle clean install.
Получим довольно малоинформативную ошибку: java.lang.ExceptionInInitializerError (no error message). Поэтому добавим флаг —stacktrace, который даст больше информации:
Для того, чтобы избавиться от этой ошибки, добавляем знакомый аргумент add-opens в системную переменную GRADLE_OPTS:
Подробнее можно прочитать здесь и здесь.
Перезапускаем сборку Gradle, получаем новую ошибку:
В результат длительного исследования оказывается, что ситуация не так проста, как для Maven. Дело в том, что во время сборки в Gradle запускается несколько JVM процессов, одни процессы могут порождать другие. Можно сделать fork процессов и главное, чтобы все они использовали одни и те же аргументы JVM. С помощью GRADLE_OPTS добиться этого невозможно. Поэтому была придумана новая системная переменная JDK_JAVA_OPTIONS, которая используется всеми JVM процессами в Gradle (подробней о ней здесь). Более того, начиная со сборки b157 появилась системная переменная JDK_JAVAC_OPTIONS для передачи параметров в Java компилятор (javac). Установим JDK_JAVA_OPTIONS в следующее значение:
И перезапустим сборку. У нас выпало исключение:
Такой ошибки у нас еще не было. Суть ее в том, что мы хотим получить доступ к классам из JDK, которые находятся в пакетах, не указанных модулем как экспортируемых (exported) в module-info.java. Это можно обойти, если добавить в JAVA_JDK_OPTIONS «—add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED»
Теперь сборка прошла успешно. Запустим тесты: gradle test. Тесты посыпались с теми же ошибками, что и в Maven. Это можно исправить, просто указав аргументы JVM в блоке test:
Мы все еще используем source/target compatibility 1.8 в нашем проекте. Если мы поменяем их на 1.9, то при компиляции получим ошибку:
Все потому что мы перешли на 1.9, а значит должны оформлять наши проекты в модули, писать module-info.java и в них указать, чтобы нам нужен (requires) тот или иной модуль из JDK. Ситуация становится интересней для Maven/Gradle зависимостей. Здесь можно прочесть о том, как конфигурировать модули, если они содержат одинаковые пакеты.
Однако пока Gradle еще не полностью готов к Java 9, вы можете запускать его под Java 8, а компилировать проект под Java 9. Для этого нужно в скрипте сборки указать конфигурацию под версию:
Тестирование REST-контейнеров
Так как я в своем докладе рассказывал про REST-контейнеры и веб-сервера, то интересно будет их проверить на совместимость с Java 9, тем более что это достаточно сложные технологии.
Начнем с Ratpack. Это довольно новый сервер, полностью написанный на Java 8 и основанный на Netty. Если мы запустим его, то получим исключение:
Нам нужно лишь добавить модуль java.activation при старте сервера, чтобы это починить:
Теперь перейдем к Apache CXF. Для него нужно указать другой модуль:
Для запуска приложения с Jersey (JAX-RS) я использую Jetty сервер. Jersey тоже требует предыдущий модуль.
Я думаю, что вы знаете либо слышали, что летом этого года выходят Spring Framework 5 и Spring Boot 2.0, специально приуроченные к выходу JDK 9 и даже использующие некоторые ее возможности. Пока они не вышли, интересно проверить текущие версии (Spring Boot 1.5.3 и Spring Framework 4.3.8) на «запускаемость».
Spring Boot поддерживает три встроенных сервера: Tomcat, Jetty и Undertow. К сожалению, у меня не получилось запустить REST-приложение ни на одном из них. Оно просто зависало на стадии загрузки.
JMeter — одна из наиболее популярных утилит для тестирования веб-приложений. Она может работать как в консольном варианте, так и в режиме с UI. Готово ли оно к Java 9? Если мы возьмем последнюю версию (3.2) и запустим jmeter.bat, то увидим в консоли ошибку:
Это достаточно странное сообщение, которое судя по всему печатает не JVM. Если мы заглянем в исполняемый файл, то увидим целые блоки кода, где сначала выполняется команда «java-version», потом ее результат парсится и из него выделяется старшая и младшая версии. Причем JMeter предполагает, что результат будет в формате 1.8.0.
Так как в Java 9 результат уже в формате 9.0.0 (openjdk.java.net/jeps/223), то это приводит к катастрофическим последствиям и аварийному завершению приложения. Причем JMeter даже не пишет, что версия неправильная, а просто говорит, что не может найти Java.
Выход заключается в том, чтобы удалить все эти проверки и тогда утилита запустится корректно. Однако при загрузке конфигурации мы получим исключение:
Для того, чтобы избежать подобных ошибок, нужно лишь добавить в JVM аргументы в скрипте строку:
Заключение и спасительный флаг
Как вы видите, в последний год вендорами была проделана большая работа по обеспечению совместимости с Java 9. Это тем более было трудно, что даже в этом году были некоторые изменения в API JDK. Тем не менее многие библиотеки уже успешно работают над Java 9, некоторые без каких-либо ухищрений, некоторые с дополнительными аргументами для JVM.
Тем не менее, выяснять и разбираться в настройках не так приятно, поэтому в конце марта 2017 года руководство Oracle решило упростить жизнь разработчикам и добавить новый спасительный флаг, который упростит миграцию на Java 9, но будет удален в Java 10.
Этот флаг —permit-illegal-access разрешает доступ всем анонимным модулям через reflection к коду именованных модулей и позволяет не писать многочисленные —add-opens. Единственный минус этого флага — он будет выводить предупреждения в консоль обо всех случаях несанкционированного доступа.
Однако оказалось, что, хотя этот флаг и избавляет от необходимости указывать многочисленные —add-opens, он не избавляет от необходимости указывать самого себя. Поэтому начиная со сборки b172 его предполагается заменить на флаг —illegal-access. У этого флага будет четыре значения:
- permit. Это значение по умолчанию, которое открывает доступ через reflection из всех анонимных модулей во все пакеты именованных модулей. При этом значении только один раз печатается предупреждение о попытке доступа.
- warn. Каждая попытка получить доступ через reflection приводит к выводу предупреждения.
- debug. Здесь к выводу предупреждения выводится еще и stacktrace операции.
- deny. Планируется сделать дефолтным в Java 10. Запрещает доступ через reflection для всех пакетов в именованных модулях, кроме указанных явно в —add-opens.
Подробнее можно прочесть здесь. Интересно, что на момент сборки b173 этот флаг все еще не добавлен.
Я надеюсь, что вам было полезно это руководство и миграция ваших приложений будет более простой и быстрой.
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.