Миграция Spring Boot приложения на Java 17 — сложный путь

Kate

Administrator
Команда форума

Java 17​

Давайте создадим проект на Java 17. В среде IDE переключите JDK на Java 17, а в родительском POM установите для java.version свойства значение 17.

<properties>
<java.version>17</java.version>
</properties>
Скомпилируем приложение и посмотрим, что получится… барабанная дробь, пожалуйста.

$ mvn compile
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project app-project: Fatal error compiling: java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x5a47730c) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x5a47730c -> [Help 1]

К сожалению, наш проект не скомпилировался, что неудивительно. Давайте посмотрим на эту ошибку Lombok.

Lombok​

Lombok - это java-библиотека, автоматизирующая генерацию шаблонного кода, который мы все ненавидим. Она может генерировать для нас геттеры, сеттеры, конструкторы, логирование и т. д., освобождая наши классы от загромождения шаблонным кодом.

Кажется, наша текущая версия 1.18.12несовместима с Java 17, она не может генерировать код должным образом. Глядя на журнал изменений Lombok, можно заметить, что поддержка Java 17 была добавлена в 1.18.22.

Версия 1.18.12 не указана напрямую в нашем проекте. Как и большинство общих зависимостей, она управляется зависимостями Spring Boot. Однако мы можем переопределить версию зависимости из Spring Boot.

В родительском элементе pom.xml мы можем переопределить версию Lombok через свойство:

<properties>
<lombok.version>1.18.22</lombok.version>
</properties>
Теперь, когда мы обновили версию, посмотрим, компилируется ли она:

$ mvn compile
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.5.1:compile (default-compile) on project app-backend: Compilation failure: Compilation failure:
[ERROR] /Users/chris/IdeaProjects/app/src/main/java/de/app/data/ValueMapper.java:[18,17] Unknown property "id" in result type de.app.entity.AppEntity. Did you mean "identifier"?
Класс ValueMapper делает то, что следует из названия: он сопоставляет класс Value с AppEntity с помощью MapStruct. Странно, мы только что обновили Lombok, поэтому Java-бины должны быть сгенерированы правильно. Это должно быть проблема с MapStruct, так что давайте посмотрим.

MapStruct​

MapStruct - это процессор аннотаций Java для автоматической генерации преобразователей (mapper) между Java-компонентами. Мы используем MapStruct для создания типобезопасных классов преобразования одного Java Bean в другой.

Мы используем MapStruct вместе с Lombok, позволяя Lombok генерировать геттеры и сеттеры для наших Java бинов, в то же время позволяя MapStruct генерировать преобразователи между этими bean-компонентами.

MapStruct использует сгенерированные геттеры, сеттеры и конструкторы и использует их для создания преобразователей.
После обновления Lombok до версии 1.18.22преобразователи больше не создаются. Lombok внес серьезное изменение в версию, 1.18.16требующую дополнительного процессора аннотаций lombok-mapstruct-binding. Давайте добавим этот обработчик аннотаций в maven-compiler-plugin:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Этого было достаточно, чтобы скомпилировать код и запустить наши модульные тесты. К сожалению, наши интеграционные тесты все еще не работают и выдают следующую ошибку:

$ maven verify
...
org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [ApplicationIT.class];
Caused by: org.springframework.core.NestedIOException: ASM ClassReader failed to parse class file – probably due to a new Java class file version that isn’t supported yet: file [ApplicationIT.class];
Caused by: java.lang.IllegalArgumentException: Unsupported class file major version 61
at org.springframework.asm.ClassReader. (ClassReader.java:196)
Давайте посмотрим на эту ошибку ASM.

ASM​

ASM - это среда для манипулирования байтовым кодом Java. ASM используется CGLIB, который, в свою очередь, используется Spring для АОП.

В Spring Framework AOP прокси - это динамический прокси JDK или прокси CGLIB.
Spring, используя CGLIB и ASM, генерирует прокси-классы, несовместимые со средой выполнения Java 17. Spring Boot 2.3 зависит от Spring Framework 5.2, который использует версию CGLIB и ASM, несовместимую с Java 17.

Обновление библиотек CGLIB или ASM на этот раз невозможно, поскольку Spring переупаковывает ASM для внутреннего использования. Придется обновить Spring Boot.

Spring Boot​

Как упоминалось ранее, наш проект в настоящее время использует Spring Boot 2.3.3-RELEASE. Когда-то это мог быть последняя версия исправления для Spring Boot 2.3.x, но в настоящее время она находится на уровне 2.3.12.RELEASE.

Согласно документу поддержки Spring Boot, Spring Boot 2.3.x достиг EOL в мае 2021 года (версия OSS). Уже одного этого достаточно для обновления, кроме желания использовать Java 17. Дополнительную информацию см. в политике поддержки Spring Boot.

Поддержка Spring Boot и Java 17​

Я не нашел официального заявления о поддержке Java 17 для Spring Boot 2.5.x или Spring Framework 5.3.x. Они объявили, что Java 17 будет базовой версией Spring Framework 6, что подразумевает официальную поддержку Java 17 начиная с Spring 6 и Spring Boot 3.

При этом, как говорится, они сделали много работы для поддержки Java 17 в Spring Framework 5.3.x и Spring Boot 2.5.x и внесли в список поддерживаемых JDK 17 и JDK 18 в Spring Framework 5.3.x. Но какая конкретная версия исправления поддерживает Java 17?

Я обнаружил ответ на этот вопрос на GitHub. Поддержка документов для Java 17 # 26767 с тегом версии 2.5.5. Это круто и достаточно для меня.

Примечания к версиям

Поскольку мы обновляем Spring Boot 2.3 до 2.5, я довольно часто ссылался на примечания к версиям для обоих. Вот они:
* Примечания к версии Spring Boot 2.4
* Примечания к версии Spring Boot 2.5
* Примечания к версии Spring Framework

Spring Boot 2.5.x​

Хотя Spring Boot 2.6.x появился несколько дней назад, давайте остановимся на Spring Boot 2.5.x. Она существует некоторое время, ошибки уже исправлены, и перепрыгивания через две второстепенные версии будет достаточно. Она официально поддерживается до мая 2022 года, поэтому устраивает нас. Надеемся, что после того, как мы перейдем на 2.5.7, переход к 2.6.xстанет проще.

Итак, на сегодняшний день последняя версия Spring Boot 2.5.x - это 2.5.7. У нас есть версия Spring Boot, которая поддерживает Java 17, давайте сделаем это изменение.

В родительском POM обновим родительский файл до spring-boot-starter-parent:2.5.7.

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
</parent>
Обратите внимание на суффикс -RELEASE, отсутствующий в новой версии. Spring обновила свою схему управления версиями, которую Spring Boot принял в версии 2.4.0.

ВАЖНОПрежде чем продолжить, удалите переопределение зависимости Lombok, которое мы добавили ранее, поскольку Spring Boot 2.5 уже определяет зависимость от Lombok 1.18.22.
Мы обновили версию Spring Boot, и теперь начинается самое интересное.

JUnit и отсутствующее свойство spring-boot.version​

Моя IDE сообщает, что свойство spring-boot.versionбольше не определено. Оно было удалено из spring-boot-dependencies, кажется, оно было введено случайно, и его не должно было там быть. Ой.

Мы используем это свойство, чтобы исключить junit-vintage-engine из нашего проекта, поскольку мы уже обновили все наши тесты до JUnit 5. Это запрещает кому-либо случайно использовать JUnit 4.

Мы исключили junit-vintage-engineиспользуя свойство spring-boot.version:

<dependencyManagement>
<dependencies>
<dependency>
<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>
</dependency>
</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 следующую зависимость:

<dependency>
<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>
</dependency>

Jackson​

Jackson - это библиотека инструментов обработки данных, например для сериализации и десериализации JSON в компоненты Java и обратно. Она может обрабатывать многие форматы данных, но мы используем его для JSON.

После обновления до Spring Boot 2.5.7 некоторые из наших тестов завершились неудачно со следующей ошибкой:

[ERROR] java.lang.IllegalArgumentException: Java 8 date/time type `java.time.OffsetDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.LinkedHashMap["updateRecordRequest"]->io.swagger.v3.oas.models.media.ObjectSchema["properties"]->java.util.LinkedHashMap["since"]->io.swagger.v3.oas.models.media.DateTimeSchema["example"])
Об этой проблеме уже сообщалось на GitHub, и, как всегда, команда Spring дает отличное объяснение проблемы и способов ее решения.

С конфигурацией по умолчанию Spring Boot 2.5 сериализация типов java.time.* в JSON должна работать в 2.5 точно так же, как в 2.4 и ранее. ObjectMapper будет автоматически сконфигурирован с модулем JSR-310 и java.time.* типы будут сериализованы в формате JSON в их ожидаемой форме.
Здесь изменилось то, что происходит, когда модуль JSR-310 недоступен для Jackson. Из-за изменения в Jackson 2.12 это теперь приведет к сбою сериализации, а не к тому, чтобы Jackson сбоил и сериализовался в неожиданный формат.
Да, вы правильно прочитали, в предыдущих версиях Jackson вместо сбоя она сериализовалась во что-то неожиданное. Ух ты. Это было исправлено в jackson-databind:2.12.0. Более быстрый Jackson теперь быстрее выдает ошибку (спасибо @jonashackt за эту шутку).

Автоконфигурация Jackson

Spring Boot обеспечивает автоконфигурацию Jackson и автоматически объявляет полностью настроенный компонент ObjectMapper. Используя IDE, я нашел все места, где создавался экземпляр ObjectMapper. В одном приложении мы объявляли наш собственный компонент, который я удалил, и реорганизовали весь код, в котором экземпляр создается локально. Полностью полагаясь на автоматическую настройку.

Jackson можно настроить без определения собственного bean-компонента ObjectMapper, используя свойства или класс Jackson2ObjectMapperBuilderCustomizer. Помимо официальной документации, вам поможет Baeldung.

Самый важный вывод заключается в следующем:

Как описано в документации, определение ObjectMapper или вашего собственного Jackson2ObjectMapperBuilder отключит автоконфигурацию. В этих двух случаях регистрация модуля JSR 310 будет зависеть от того, как был настроен ObjectMapper или использовался построитель.
Дважды проверьте, что модуль com.fasterxml.jackson.datatype:jackson-datatype-jsr310 находится в пути к классам, и он будет автоматически зарегистрирован в ObjectMapper.

Я видел много проектов, в которых ObjectMapper настраивается путем повторного создания bean-компонента или создается локально внутри класса или метода. Это редко бывает необходимо и может привести к ошибкам и дублированию конфигураций. и т. д. Не говоря уже о том, что создание ObjectMapper - дорогое удовольствие. Он потокобезопасный, поэтому его нужно создать один раз и повторно использовать.

Теперь, когда наше приложение правильно использует ObjectMapper, давайте взглянем на одну из наших библиотек.

Валидатор запросов Swagger от Atlassian

Валидатор запросов Swagger от Atlassian - это библиотека для валидации запросов/ответов Swagger/OpenAPI 3.0. Мы используем ее в наших тестовых примерах, особенно в библиотеке swagger-request-validator-mockmvc. Если вы еще не пользуетесь этой библиотекой, попробуйте ее, она довольно крутая.

Мы используем старую версию этой библиотеки, которая не использует автоконфигурацию Spring Boot Jackson и не регистрирует JavaTimeModule в собственном ObjectMapper. Они исправили эту проблему, замеченную в версии 2.19.4. После обновления версии библиотеки тесты снова заработали.

Это была единственная библиотека, которую мы использовали, у которой были какие-либо проблемы с Jackson, но у вас могут быть и другие. Обязательно используйте последнюю версию ваших библиотек, которая обычно включает такие исправления.

Итоги первого дня​

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

Надеюсь, вы присоединитесь к нам на второй день, потому что наше путешествие только началось. Когда мы продолжим, мы увидим, что наши интеграционные тесты не работают, и подробно рассмотрим, почему.

Я хотел бы услышать о вашем опыте миграции Spring Boot. Пожалуйста, оставляйте комментарии или не стесняйтесь обращаться ко мне. Мы кодоцентричны и готовы помочь.

[Обновление] Забыл упомянуть, что мы переходим с Java 11.

 
Сверху