Java 17 — нова LTS-версія — вийшла на ринок майже рік тому, але й досі не набула значної популярності. Про те, чому на неї таки слід перейти я детально розповідав на конференції NIX MultiConf та у своїй попередній статті на Highload.
У цій статті я опишу основні кроки та особливості міграції проєкту з Java 11 на Java 17. Я намагатимуся розглянути можливі помилки та варіанти їх вирішення.
Одразу наголошую: я розглядатиму міграцію з програмою Spring Boot. Це один із найпопулярніших фреймворків, який використовують більшість Java-розробників.
Щоправда, офіційна підтримка Java 17 починається зі Spring 6 та Spring Boot 3, та ці версії ще у розробці. Остання ж версія Spring Boot — 2.6.7. Її і розглянемо. Хоча вона офіційно не підтримується на Java 17, розробники Spring доклали багато зусиль, аби версії Spring Boot від 2.6.5 вже певною мірою працювали з новою LTS.
Для початку вам потрібно у pom.xml
знайти properties
та змінити версію з поточної на 17-ту. Це може виглядати так:
<properties> <java.version>11</java.version> </properties>
на
<properties> <java.version>17</java.version> </properties>
Проте, скоріше за все, при використанні будь-яких залежностей у вас станеться помилка. Наприклад, може з’явитися повідомлення на кшталт:
error: release version 17 not supported
Спочатку може бути не зрозуміло, чому так сталося. Тому спробуємо крок за кроком розібратися з проблемою та вирішити її.
Найперша помилка — це Lombok. Так називається Java-бібліотека, яка автоматизує генерацію шаблонного коду. Вона може генерувати гетери, сеттери, конструктори, логування тощо, звільняючи класи від захаращення шаблонним кодом. При міграції з 11-ї на 17-у версії Java ви отримуєте наступну помилку:
java.lang.IllegalAccesError: class lombok.javac.apt.LombokProcessor cannot access classcom.sun.tools.javac.proseccing.JavacProcessingEnvironment
Це все стосується версії Lombok 1.18.22. На момент міграції мого застосунку в мене була версія 1.18.12. Щоб зрозуміти причину цієї помилки, потрібно поглянути на JEP 396, який додав сувору інкапсуляцію внутрішніх компонентів JDK за замовчуванням.
Цей JEP був реалізований ще у 16-й версії Java, і саме через нього Lombok більше не має можливості використовувати внутрішні компоненти JDK.
Спочатку розробники Lombok вирішили використовувати наступну команду для скасування попереднього JEP:
--illegal-access=permit
Але це було дійсно лише для Java 16. В 17-й же версії цю команду прибрали. Тому в Lombok вигадали інше рішення. Вам потрібно додати у maven-compiler-plugin
аргументи. Таким чином ви даєте доступ до потрібних компонентів Lombok:
<compilerArgs> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg> <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg> </compilerArgs>
Однак вручну вносити аргументи не дуже зручно. Тому в Lombok випустили версію 1.18.22, в якій це робиться автоматично — вам достатньо лише додати цю залежність. Задля цього розробники за допомогою рефлексії змінили видимість модулів — і вже після цього ви маєте до них доступ.
Так, це не дуже сек’юрно, але все ж таки працює.
MapStruct — це процесор анотацій Java для автоматичного генерування перетворювачів (mapper) між Java-компонентами.
MapStruct використовує згенеровані гетери, сеттери та конструктори для створення перетворювачів. Після оновлення Lombok до версії 1.18.22 перетворювачі більше не створюються саме через ту помилку — через те, що був реалізований JEP 396, який додав сувору інкапсуляцію внутрішніх модулів. Щоб обійти цей момент, необхідно додати обробник анотацій lombok-mapstruct-binding
до maven-compiler-plugin
:
<path> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </path>
Після цього все має працювати без проблем.
ASM — це середовище для маніпулювання байтовим кодом Java. ASM використовує CGLIB, який, у свою чергу, використовується Spring для AOP.
У Spring Framework AOP-проксі — це динамічний проксі JDK або проксі CGLIB. Spring використовує CGLIB та ASM, генерує проксі-класи, несумісні з середовищем виконання Java 17. Spring Boot нижче, аніж 2.4, і залежить від Spring Framework 5.2, який використовує версію CGLIB та ASM, несумісну з Java 17.
Але оновлення бібліотек CGLIB або ASM неможливе, оскільки Spring перепаковує ASM для внутрішнього використання. Тобто єдиний вихід із цієї ситуації — оновити Spring Boot. Без цього ASM не запрацює.
У новій версії Spring Boot зі spring-boot-dependencies
було видалено властивість spring-boot.version
. Розробники використовували її для багатьох задач, скажімо, щоб виключати якусь залежність.
Ось приклад із винятком junit-vintage-engine
:
<dependencyManagement> <dependencies> <dependecy> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependecy> </dependencies> </dependencyManagement>
На щастя, тепер ми можемо видалити цей блок, оскільки Spring Boot 2.4 вилучив Vintage Engine JUnit 5 зі стартера spring-boot-starter-test
.
Однак якщо ваш проєкт досі використовує JUnit 4 і ви бачите помилки компіляції типу java: package org.junit does not exist
— то це тому, що старий рушій був видалений.
Старий рушій відповідає за виконання тестів JUnit 4 разом із тестами JUnit 5. Якщо ви не можете через якісь обставини перенести тести на JUnit 5, додайте до pom
наступну залежність:
<dependecy> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> </exclusion> </exclusions> </dependecy>
Це повністю вирішить описану проблему.
Наступна можлива проблема пов’язана з Jackson. Jackson — це бібліотека інструментів обробки даних, наприклад, для серіалізації та десеріалізації JSON у компоненти Java і навпаки. Вона може обробляти багато форматів даних, але найчастіше її використовують для роботи з JSON.
Після оновлення до Spring Boot 2.6.7 виникає наступна помилка:
java.time.OffsetDateTime not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310”
Причина — модуль JSR-310 недоступний для Jackson. Через зміну Jackson 2.12 тепер це призводить до збою серіалізації, а не до того, щоб Jackson серіалізувався в несподіваний формат.
Існує два рішення цієї проблеми. Ви можете створити mapper
власноруч. Для версії 2.10 та новішої через JsonMapper.buider це виглядає наступним чином:
ObjectMapper mapper = JsonMapper.buider() .addModule(new JavaTimeModule()) .build();
Для старішої версії це можна зробити через mapper.registerModule
:
ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule());
Однак все це не дуже гарний варіант, адже створення мапперів власними руками потребує зайвого часу та зусиль. Тому пропоную інший спосіб — спробувати автоконфігурацію Jackson.
Spring Boot забезпечує автоконфігурацію Jackson та автоматично оголошує повністю налаштований компонент ObjectMapper
. Jackson можна налаштувати без визначення власного Bean-компонента ObjectMapper
, використовуючи властивості або клас Jackson2ObjectMapperBuilderCustomizer
.
Необхідно перевірити, що модуль com.fasterxml.jackson.datatype:jackson-datatype-jsr310
знаходиться в дорозі до класів (тобто у classpath
) — і він буде автоматично зареєстрований у ObjectMapper
.
ObjectMapper
— потокобезпечний, тому його можна створити раз і використовувати повторно.
Swagger від Atlassian являє собою бібліотеку для валідації запитів та відповідей Swagger/OpenAPI 3.0. Стара версія цієї бібліотеки не використовує автоконфігурацію Spring Boot Jackson та не реєструє JavaTimeModule
у своєму ObjectMapper.
Але після оновлення версії бібліотеки тести знову працюють. Тому потрібно обов’язково використовувати останню версію бібліотек, яка зазвичай містить усі необхідні виправлення.
Загалом це найбільш помітні помилки, з якими я зіткнувся при переході на Java 17 і які мені здалися вартими уваги. Звичайно, на практиці проблем може бути більше, але вони здебільшого незначні та потребують не стільки особливих підходів, скільки уважності і додаткового часу для вирішення. На мою думку, це все виправдовується перевагами останньої LTS-версії: вона більш функціональна та швидка.
Тому не бійтеся мігрувати свої проєкти з Java 11 на Java 17. Це не так страшно, як може спочатку здаватися.
Формат компанії — аутстаф, аутсорс чи продукт — суттєво впливає не лише на умови співпраці,…
Блогер та розробник Джозеф Круз розповів, чому так важко прочитати чужий код. Пропонуємо вам переклад…
«Розкажіть коротко про себе» — це одне з найпоширеніших питань на співбесіді. І, як не…
Ми всі часто говоримо про те, що спілкування в команді — це дуже важливо. Важлива…
Блогер та розробник Джозеф Круз розповів про підводні камені роботи продакт-менеджера та те, з чим…
Кілька місяців тому я скептично ставився до NestJS. Фреймворк здавалося простою обгорткою над Express або…