Привет! Большинство разработчиков не спешат обновляться до новых версий Java. Многие опасаются, что все сломается, что появятся скрытые баги, что это займет очень много времени. Сегодня мы поделимся опытом перевода IGA-системы Solar InRights с Java 8 на актуальную Java 16, расскажем, для чего мы это сделали и почему именно сейчас. Подробно опишем, какие проблемы могут возникнуть при обновлении и как их устранить, а главное – поделимся тем, что мы в итоге получили.

Ранее в разработке мы использовали Java 8. Эта версия платформы и сейчас является достаточно стабильной и функциональной, в целом это понятная и удобная версия.

Однако в экосистеме произошло несколько важных событий, которые подтолкнули нас к переходу:

  • Была выпущена Java 9 со значительными архитектурными изменениями. Начиная с этой версии платформа Java является модульной, появляются разные выпуски JVM с различным набором фич. Эти изменения ломают обратную совместимость, и большинство программ, написанных на Java 8, не запускаются на Java 9+.

  • С выпуском Java 11 компания Oracle представила новую модель лицензирования и подписки. Теперь нужно особенно внимательно следить за тем, какую версию и где мы используем, чтобы не нарушить лицензию.

  • Релиз Java 16 включает множество новых возможностей в языке Java. Да, кто-то обязательно возразит, что аргумент слабый. Можно и дальше пользоваться Java 8, где все понятно, просто и удобно. Но с новыми синтаксическими конструкциями Java 16 писать код удобнее, а отслеживать ошибки проще. Наряду с уже перечисленными причинами это довольно приятный бонус.

У нас появилось свободное время на развитие системы, поэтому мы решили перейти на актуальный выпуск Java, и чем раньше это произойдет, тем легче будет процесс перехода. Версии с 11 по 16 называются промежуточными, потому что у них короткий период поддержки – 6 месяцев. На сентябрь 2021 года запланирован LTS (Long Term Support) релиз Java 17.

Так мы решили перейти на Java 16, чтобы затем быстро перейти на LTS Java 17.

С чем имеем дело

Система Solar inRights в собранном виде – это веб-приложение в виде war-файла, которое запускается на сервере Tomcat. Для сборки используем Maven. Код организован по функциональным модулям. Один модуль – это один maven-проект.

Система состоит из нескольких слоев:

Мы активно используем Spring для инверсии зависимостей, Hibernate для абстракции над логикой баз данных и JAXB для сериализации данных. Для написания тестов используем язык Groovy, а запускаем тесты с помощью TestNG.

В результате получается модульный монолит, в classpath которого собраны ресурсы от всех зависимостей.

Обновление на Java 16

На текущий момент новейшая стабильная версия JDK – это 16. Это preview release, значит некоторые языковые конструкции могут быть изменены или отсутствовать в следующей версии Java.

Мы решили, что будем собирать систему с помощью Liberica JDK 16 с русскоязычной поддержкой, но нам подойдет и OpenJDK, и любая открытая сборка JDK без специальных особенностей.

Просто взять и собрать систему на JDK 16 не получилось. Видим море ошибок, говорящих о том, что наш код обращается к закрытым частям платформы. Теперь нельзя обращаться к com.sun.*,  java.security.* и прочим внутренностям JDK.

Изменяем работу с XML на использование открытых API.

Было:

import
com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;
import com.sun.org.apache.xml.internal.utils.XMLChar;
import com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl;

Стало:

import javax.xml.datatype.XMLGregorianCalendar;
import org.apache.xerces.util.XMLChar;
import org.w3c.dom.Node;

При сборке Maven отказывается читать наши Mojo, потому что не знает, как читать байткод классов, скомпилированных в Java 16. Обновляем версии зависимостей maven-plugin-tools (на текущий момент это 3.6.1):

<dependency>
		<groupId>org.apache.maven.plugin-tools</groupId>
		<artifactId>maven-plugin-tools-generators</artifactId>
		<version>3.6.1</version>
</dependency>
<dependency>
		<groupId>org.apache.maven.plugin-tools</groupId>
		<artifactId>maven-plugin-tools-api</artifactId>
		<version>3.6.1</version>
</dependency>
<dependency>
		<groupId>org.apache.maven.plugin-tools</groupId>
		<artifactId>maven-plugin-tools-annotations</artifactId>
		<version>3.6.1</version>
</dependency>
<dependency>
		<groupId>org.apache.maven.plugin-tools</groupId>
		<artifactId>maven-plugin-annotations</artifactId>
		<version>3.6.1</version>
</dependency>
<dependency>
		<groupId>org.apache.maven.plugin-tools</groupId>
		<artifactId>maven-plugin-tools-java</artifactId>
		<version>3.6.1</version>
</dependency>

У Maven также будут проблемы с чтением конфигурации из javadoc. Исправляем формат javadoc в Mojo классах.

Было:

/** @parameter default-value="${project}" */
private MavenProject project;

Стало:

/**
* @parameter default-value="${project}"
*/
private MavenProject project;

Обновляем версию плагина Maven для запуска TestNG:

<dependency>
		<groupId>org.apache.maven.surefire</groupId>
		<artifactId>surefire-testng</artifactId>
    <version>2.22.23.0.0-M5</version>
</dependency>

Теперь нужно обновить библиотеки проксирования и манипуляций с байткодом – все, что изменяет внутреннюю структуру классов. У нас получилось так:

<dependency>
		<groupId>org.ow2.asm</groupId>
		<artifactId>asm</artifactId>
		<version>9.1</version>
</dependency>
<dependency>
		<groupId>org.apache.bcel</groupId>
		<artifactId>bcel</artifactId>
		<version>6.5.0</version>
</dependency>
<dependency>
		<groupId>org.codehaus.plexus</groupId>
		<artifactId>plexus-classworlds</artifactId>
		<version>2.6.0</version>
</dependency>

Поскольку Java 16 является preview версией, необходимо добавить параметр сборки --enable- preview. Для Maven можно создать файл .mvn/jvm.config со следующим содержанием:

--enable-preview

Это удобно, потому что опция записана в одном месте, и ее можно удалить, когда мы перейдем на LTS выпуск Java 17.

Запуск тестов показал, что внутри нам требуется доступ к инкапсулированным модулям платформы:

java.lang.IllegalAccessError: class ... (in unnamed module @0x568bf312)
cannot access class ... (in module java.base) because module 
java.base does not export sun.security.util to unnamed module @0x568bf312

В несколько итераций получаем подходящий список параметров запуска:

--enable-preview
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.regex=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
--add-opens java.base/java.util.stream=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.xml/javax.xml.datatype=ALL-UNNAMED
--add-opens java.xml/javax.xml.namespace=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.xerces.internal.jaxp.datatype=ALL- UNNAMED
--add-exports java.management/sun.management=ALL-UNNAMED
--add-exports java.xml/com.sun.org.apache.xerces.internal.jaxp.datatype=ALL- UNNAMED

Последний шаг – обновить сервер приложений Tomcat. Сейчас новейшей версия – 10, и в ней разработчики требуют, чтобы мы переписали все использования javax.* на jakarta.* – однако нам подойдет Tomcat 9, его последние сборки тоже поддерживают Java 16.

Наконец, система полностью собрана на Java 16 и может быть запущена на любом JDK. Мы  используем Liberica JDK и OpenJDK.

При тестировании мы увидели небольшие различия в поведении системы по сравнению с Java 8:

  • Форматирование XMLGregorianCalendar теперь использует формат, настроенный в системе по умолчанию, вместо единого заданного формата. Если мы хотим сохранить прежний формат, мы должны явно его указать.

  • Теперь не допускается хранение в resources файлов, имеющих кириллические символы в названии.

  • JVM теперь не завершит работу, пока все потоки программы не завершат выполнение. Это хороший повод разобраться с ThreadPool, потому что Tomcat не завершит работу до тех пор, пока они не будут закрыты.

Что дальше

В целом обновление прошло быстро, гладко, без сюрпризов. Что мы получили результате:

  • Больше не опасаемся лицензионных ограничений, связанных с Java 8.

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

  • Новые синтаксические конструкции в языке Java, которые уже давно есть в Kotlin и Groovy (например, улучшенный instanceof, record-классы, текстовые блоки).

  • Возможность обновить сторонние зависимости, фреймворки, инструменты.

Java 16 является промежуточным обновлением платформы с коротким циклом поддержки. Мы ожидаем стабильного релиза Java 17 в сентябре, чтобы сразу перевести Solar InRights на LTS выпуск платформы. А это в свою очередь, вместе с обновлением фреймворков и других библиотек, повысит качество продукта в целом.

Автор: Олег Гетманский, архитектор информационных систем "Ростелеком-Солар"

Комментарии (8)


  1. luckman
    10.08.2021 11:03

    add-opens и add-exports знатные костыли, конечно.

    Переводил проект с автотестами на 16ую, но даже в таком небольшом проекте, всего лишь с одним десятком зависимостей, полноценно избавиться не удалось. Подозреваю, что эти строчки с нами надолго теперь.


    1. inRights Автор
      10.08.2021 11:20
      +1

      Похоже на то, потому что даже Tomcat для Java 9+ добавляет свои --add-opens

      Олег Гетманский


  1. rrrad
    10.08.2021 15:09

    А можете уточнить по поводу "JVM теперь не завершит работу, пока все потоки программы не завершат выполнение"? В javadoc-е для 16й верии на Thread.setDaemon написано "The Java Virtual Machine exits when the only threads running are all daemon threads.".

    Получается, что либо это не совсем так, либо tomcat добавляет свой shutdown hook, который ждёт завершения daemon thread-ов?

    Очень неудобно было бы подвесить какой-нибудь сервис при перезапуске из-за фонового потока, который по какой-то причине завис в цикле, в который забыли прописать проверку на то, что поток прерван.


    1. inRights Автор
      10.08.2021 18:07
      +2

      Привет! Мы управляем потоками через ExecutorService, который по умолчанию создает non-daemon потоки. И, похоже, что сейчас Java 16 работает строго по доке. Поэтому нужно либо явно передавать ThreadFactory, которая устанавливает для потоков setDaemon(true), либо аккуратно завершать ThreadPool при останове приложения.

      Олег Гетманский


      1. rrrad
        10.08.2021 20:13

        В javadoc'е JDK 16 указано, что приложение завершается либо когда все не-daemon потоки завершаются, либо при вызове System.exit.

        Я так понимаю, вы и версию tomcat-а сменили, возможно в ней почему-то отказались от завершения по System.exit.


  1. kacetal
    11.08.2021 11:26
    +1

    16 версия не является preview версией. Просто в ней есть некоторые вичи языка которые находятся в статусе preview. И чтобы ими пользоваться надо добавлять этот флаг.

    Но тоже самое будет и в 17 версии, например расширенный свитч, который матчит классы и null.

    Собственно говоря этот флаг нужен только если вы пользуетесь вичами языка которые не были окончательно выпущены.


  1. AlexeyOs
    11.08.2021 15:01

    Насколько большой проект переводили на Java 16?

    Почему вы везде указываете автора, а просто не сделаете учётную запись с описанием в профиле для автора и не добавите его в вашу организацию на хабр?


    1. inRights Автор
      11.08.2021 15:03
      +1

      Привет!

      1. Solar InRights состоит из примерно 400 тысяч строк кода и 4 тысяч классов без учета сторонних зависимостей.

      2. Сакрального смысла в этом нет :)

      Олег Гетманский