Боль и страдания перехода на Java 9
Обычно я, как и остальные, перехожу на новую версию явы лишь через полгода-год после релиза. Но в этот раз, вернувшись из стать д’артаньяном пройтись по граблям самостоятельно. Забегая наперед — все это было не зря. Хотя результат получился и не таким хорошим, как я ожидал:
Production instance после перехода на Java 9
Проект у нас довольно простой в плане стека технологий. Мы, например, не используем Spring, Hibernate. А всю модель храним в памяти. Поэтому память — довольно критический для нас ресурс, как и CPU. Текущая нагрузка в 13к req/s каждый месяц становится все больше и больше и иногда не дает мне спать.
Помимо типичных http/s, websockets, mqtt мы также используем собственный полубинарный, полутекстовый протокол на netty. Поэтому идея перехода на
В общем, вернувшись из отпуска, первым делом я заапдейтил все библиотеки и плагины, которые у нас используются. Список получился таким:
<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) для моих мавен-модулей, чтобы по настоящему мигрировать на
И тут сразу начались проблемы. Первый же модуль:
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 конференции.