Боль и страдания перехода на Java 9

Обычно я, как и остальные, перехожу на новую версию явы лишь через полгода-год после релиза. Но в этот раз, вернувшись из 3-недельного отпуска, я был полон сил и решимости стать д’артаньяном пройтись по граблям самостоятельно. Забегая наперед — все это было не зря. Хотя результат получился и не таким хорошим, как я ожидал:

Production instance после перехода на Java 9

Проект у нас довольно простой в плане стека технологий. Мы, например, не используем Spring, Hibernate. А всю модель храним в памяти. Поэтому память — довольно критический для нас ресурс, как и CPU. Текущая нагрузка в 13к req/s каждый месяц становится все больше и больше и иногда не дает мне спать.

Помимо типичных http/s, websockets, mqtt мы также используем собственный полубинарный, полутекстовый протокол на netty. Поэтому идея перехода на 9-ку была оправданной. Так как JEP 254: Compact Strings и JEP 280: Indify String Concatenation.

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

<maven-release-plugin.version>2.5.3</maven-release-plugin.version>
<maven-compiler-plugin.version>3.7.0</maven-compiler-plugin.version>
<maven-shade-plugin.version>3.1.0</maven-shade-plugin.version>
<maven-surefire-plugin.version>2.20.1</maven-surefire-plugin.version>
<maven-checkstyle-plugin.version>2.17</maven-checkstyle-plugin.version>

<netty.version>4.1.16.Final</netty.version>
<netty.boring.ssl.version>2.0.6.Final</netty.boring.ssl.version>
<log4j2.version>2.9.1</log4j2.version>
<jackson-databind.version>2.9.1</jackson-databind.version>
<disruptor.version>3.3.6</disruptor.version>
<async-http-client.version>2.1.0-alpha25</async-http-client.version>
<twitter4j-core.version>4.0.2</twitter4j-core.version>
<postgresql.version>42.1.4</postgresql.version>
<HikariCP.version>2.7.1</HikariCP.version>
<qrgen.version>2.2.0</qrgen.version>
<bcpg-jdk15on.version>1.57</bcpg-jdk15on.version>
<acme4j-client.version>0.12</acme4j-client.version>
<javaee-api.version>8.0</javaee-api.version>
<javax.mail.version>1.6.0</javax.mail.version>
<javax.activation.version>1.2.0</javax.activation.version>

Это очень важный шаг. Без апдейтов плагинов и либ на новую версию явы лучше не переходить.

Настроил maven-compiler-plugin на девятку:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.7.0</version>
   <configuration>
       <source>9</source>
       <target>9</target>
   </configuration>
</plugin>

И вуаля — все завелось. Единственное, что при запуске JVM вываливалось сообщение в консоль:

WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by io.netty.util.internal.ReflectionUtil (file:/home/azureuser/server-0.28.0-SNAPSHOT.jar) to constructor java.nio.DirectByteBuffer(long,int) WARNING: Please consider reporting this to the maintainers of io.netty.util.internal.ReflectionUtil 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

Так как часть наших юзеров используют приватные сервера, оставлять это сообщение без внимания было нельзя. Нас засыпали бы вопросами: «Ааа, что это такое?». Сообщение можно было бы пофиксить через опцию JVM. Но это был не вариант, так как лишь часть пользователей выполняют инструкции. Поэтому, недолго думая, я закинул вопрос на СО. И получил в ответ довольно забавный хак:

public static void disableWarning() { 
    try { 
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); 
        Unsafe u = (Unsafe) theUnsafe.get(null);
        Class cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
        Field logger = cls.getDeclaredField("logger");
        u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
    } catch (Exception e) {
        // ignore
    }
 }

Но он сработал.

Создание module-info.java

Теперь предстояло создать ява-модули (module-info.java) для моих мавен-модулей, чтобы по настоящему мигрировать на 9-ку, а не просто скомпилить под нее весь код.

И тут сразу начались проблемы. Первый же модуль:

module cc.blynk.acme {
    requires acme4j.client;
    requires acme4j.utils;
}

Модуль, который генерит Let’s Encrypt сертификаты, отказывался собираться:

Error:java: the cc.blynk.acme module reads package org.shredzone.acme4j.util from both acme4j.client and acme4j.utils

Ошибка говорит нам, что пакеты с одним и тем же именем подтягиваются из двух разных модулей. Чего делать нельзя. Есть несколько способов решить эту проблему, но все они требуют изменений на стороне самой библиотеки. Поэтому я быстренько забросил тикет. К счастью, от этой части зависел только 1 модуль, поэтому я переключился на создание модулей в других пакетах.

Вторая неприятность ждала меня в модуле отправки писем.

module cc.blynk.server.notifications.mail {
   requires log4j.api;
   requires javaee.api;
   requires java.activation;

   exports cc.blynk.server.notifications.mail;
}

Пакет java.activation оказался устаревшим, и будет удален в следующих релизах явы. Опять вопрос на СО. В общем, пока все можно оставить как есть. Но если не нужны проблемы в будущем, то лучше заменить:

<dependency>
   <groupId>javax.activation</groupId>
   <artifactId>activation</artifactId>
   <version>1.1</version>
<dependency>

на

<dependency>
   <groupId>com.sun.activation</groupId>
   <artifactId>javax.activation</artifactId>
   <version>1.2.0</version>
</dependency>

P. S. Обычно javax.activation пакет идет в зависимостях к

<dependency>
   <groupId>com.sun.mail</groupId>
   <artifactId>javax.mail</artifactId>
</dependency>

Сразу после добавления первого модуля отвалился maven-checkstyle-plugin:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-checkstyle-plugin:2.17:check (default-cli) on project email: Failed during checkstyle configuration: NoViableAltException occurred during the analysis of file /home/xxx/IdeaProjects/blynk-server/server/notifications/email/src/main/java/module-info.java. unexpected token: module 

Оказалось, плагин еще не готов к девятке, поэтому нужно добавить module-info.java в исключения при чекстайле:

<plugin> 
<groupId>org.apache.maven.plugins</groupId> 
<artifactId>maven-checkstyle-plugin</artifactId>
 <configuration> 
   <excludes>**/module-info.java</excludes> 
 </configuration>
</plugin>

Теперь пришел черед модулей, которые зависят от netty. У нас в проекте есть, например, такая зависимость:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-epoll</artifactId>
    <version>${netty.version}</version>
    <classifier>${epoll.os}</classifier>
</dependency>

Для библиотек, которые еще не перешли на девятку, ява автоматически генерирует имя на основе имени jar-файла. Поэтому для jar-файла транспорта выше получилось имя:

netty.transport.native.epoll

Но есть один нюанс: native — зарезервированное слово в яве. Его нельзя использовать в названии пакетов и модулей. Попытка скомпилировать этот модуль:

module core { 
    requires netty.transport.native.epoll;
 }

выдает:

module not found: netty.transport.<error>

Можно также проверить это из командной строки:

jar --file=netty-transport-native-epoll-4.1.16.Final-linux-x86‌​_64.jar --describe-module

выдает:

Unable to derive module descriptor for: netty-transport-native-epoll-4.1.16.Final-linux-x86‌_64.jar netty.transport.native.epoll: Invalid module name: 'native' is not a Java identifier

Тут решение, как и в первом случае, требует изменений на стороне библиотеки. Самым простым вариантом будет добавить предопределенное имя модуля в manifest-файл джарки:

<Automatic-Module-Name>netty.transport.epoll</Automatic-Module-Name>

Pull request c фиксом. Теперь жду фиксы от мейнтейнеров, чтобы закончить модуляризацию нашего проекта.

Еще парочка проблем

После этого, с горем пополам, удалось собрать проект с частичной модуляризацией. Но после релиза оказалось, что Java 9 нет для ARM-архитектур. И судя по всему, в ближайшее время не предвидится. Поэтому пока что собираем 2 артефакта. Это оказался самый простой вариант:

<profile>
   <id>java9</id>
   <activation>
       <jdk>9</jdk>
       <activeByDefault>true</activeByDefault>
   </activation>
   <properties>
       <jarClassifier>java9</jarClassifier>
       <source>9</source>
       <target>9</target>
       <!-- just fake property. means do not exclude anything -->
       <version.specific.exclusions>**/ccxxyy.java</version.specific.exclusions>
   </properties>
</profile>

<profile>
   <id>java8</id>
   <activation>
       <jdk>1.8</jdk>
   </activation>
   <properties>
       <jarClassifier>java8</jarClassifier>
       <source>1.8</source>
       <target>1.8</target>
       <version.specific.exclusions>**/module-info.java</version.specific.exclusions>
   </properties>
</profile>

С наскоку собрать multi-jar не получилось. Поэтому если у кого-то есть готовая конфигурация — буду рад ознакомиться.


На этом, собственно, все. Делитесь своими историями в комментах. Мы уже перевели все наши ~20 серверов на Java 9, хоть и с частичной модуляризацией. А вы?

P. S. Благодаря раннему переходу на девятку мы попали на слайд Марка Рейнхольда (head of Java) на проходящей сейчас JavaOne конференции.

Похожие статьи:
There are lots of food ordering apps on the market these days, but one that is attempting to grab a slice of the pizza market is…Slice! The app, which used to be known as MyPizza, allows the user to order from up to 10,000 different pizzerias....
Підрядники Google із компанії Appen кажуть, що у них недостатньо часу для детальної перевірки відповідей чат-бота Bard, тож іноді доводиться...
Компания ZTE анонсировала новый смартфон Nubia Prague S, который является наследником вышедшего летом ZTE Nubia My Prague. Устройство оборудовано...
Українські консульства тимчасово не прийматимуть документи від чоловіків призовного віку, повідомляє Суспільне. Йдеться,...
Меня зовут Евгений Моспан, и я в IT с 2003 года. Начинал стажером в продуктовой компании, и за время работы там прошел путь...
Яндекс.Метрика