Боль и страдания перехода на 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 конференции.

Похожие статьи:
Статья написана в соавторстве с Екатериной Сиротинской, HR-директором в Omnicore, и Дмитрием Куявцем, консультантом в области цифровых...
Оператор мобильной связи «Билайн» отмечает, что в сентябре 2015 года обеспечил своих клиентов возможностью пользоваться...
GlobalLogic, яка входить у топ-3 найбільших IT-компаній України та є частиною японського конгломерату Hitachi Group, оголосила про...
У Вас скучная работа или Вы хотите сменить род деятельности? А может, Вы практикующий тестировщик и Вам необходимо...
Анастасия Режепп, глава дизайн-студии DataArt, рассказала на конференции IT Arena Lviv, как им с коллегами удалось повысить...
Яндекс.Метрика