Как стать автором
Обновить

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

Время на прочтение11 мин
Количество просмотров25K
Автор оригинала: Chris Coltsman

Недавно была выпущена Java 17, и я очень рад появлению множества улучшений и новых функций. Вместо того, чтобы начинать с нового или недавнего проекта (где в этом азарт?), мы собираемся обновить существующее приложение Spring Boot, пока мы не сможем разработать новый код с использованием Java 17.

День первый

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

Мы склонны пренебрегать существующими приложениями, сосредотачиваясь только на активно разрабатываемых новых. Это разумно: зачем трогать работающую систему? Но для этого есть веские причины, самая важная из которых - безопасность, и Java 17 может стать отличным оправданием для того, чтобы наконец взяться за эту задачу.

Многие корпорации придерживаются политики, запрещающей версии JDK, отличные от LTS. Это делает Java 17 таким привлекательным для многих из нас. По прошествии стольких лет у нас наконец-то появилась LTS-версия, которую мы можем использовать при разработке наших корпоративных приложений.

Мы хотели бы использовать Java 17 в одном из наших существующих проектов, поэтому я надеюсь, что вы продолжите наше путешествие. Вместе мы собираемся сделать это и по пути кое-чему научиться.

Настройка

Наш проект представляет собой монорепозиторий, содержащий ~ 20 приложений Spring Boot. Все они принадлежат одному продукту, поэтому находятся в одном проекте Maven. Продукт состоит из шлюза API, предоставляющего REST API, нескольких внутренних приложений, взаимодействующих между собой с помощью Kafka и интеграции с SAP. Все приложения в настоящее время используют версию Spring Boot 2.3.3-RELEASE.

Чтобы дать вам представление о том, о чем мы говорим, все следующие проекты Spring используются в нашем проекте и одним или несколькими приложениями:

Давайте начнем.

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.

Теги:
Хабы:
Всего голосов 16: ↑15 и ↓1+18
Комментарии5

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань