Міграція застосунків на JDK 22. Частина друга

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник, хочу поділитися з вами своїм досвідом міграції проєктів з JDK 21 на JDK 22. У першій частині цієї статті я розповів про найцікавіші фічі, які увійшли до JDK 22, і навів приклади їхнього використання.

У другій частині я хочу розповісти про результати тестування продуктивності нових фіч і ті складнощі, з якими ми зіткнулися, коли переносили наші сервіси на нову версію Java.

Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про нові фічі в Java 22, розібратися в тому, як провести таку міграцію і яка від цього практична користь.

Тестуємо продуктивність Stream Gatherers

Цікаво перевірити продуктивність нового API. Для цього візьмемо вже добре знайому нам бібліотеку JMH і таку конфігурацію:

  • JMH 1.37
  • JDK 22.0.1
  • Intel Core i9, 8 cores
  • 32 GB
  • 10 ітерацій обчислення, 10 ітерацій прогріву (warm-up)

Код для тестів виглядає так (код для DayGatherer можна переглянути в першій частині статті):

 @State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
public class GathererBenchmark {
    private final List<LocalDateTime> dates = List.of(LocalDateTime.of(2024, 1, 1, 10, 0),
            LocalDateTime.of(2024, 1, 1, 17, 10),
            LocalDateTime.of(2024, 1, 1, 19, 12),
            LocalDateTime.of(2024, 1, 2, 9, 27),
            LocalDateTime.of(2024, 1, 5, 11, 17));
    private final DayGatherer gatherer = new DayGatherer();
    @Benchmark
    public List<List<LocalDateTime>> useStreamsAPI() {
        return dates.stream().collect(Collectors.groupingBy(LocalDateTime::toLocalDate))
                .values().stream().sorted(Comparator.comparing(List::getFirst)).toList();
    }
    @Benchmark
    public List<List<LocalDateTime>> useGatherers() {
        return dates.stream().gather(gatherer).toList();
    }
}

Результати benchmarks:

Benchmark                                           Mode  Cnt    Score    Error       Units
GathererBenchmark.useGatherers    avgt    5    153.405   2.260    ns/op
GathererBenchmark.useStreamsAPI  avgt    5   218.765 10.974    ns/op

Таким чином, Stream Gatherers для такого завдання та невеликій (5) кількості елементів дозволив покращити продуктивність на 30%. Тепер спробуємо перевірити їхню ефективність на великому наборі елементів, згенерувавши 100 тисяч випадкових дат протягом двох років:

private final List<LocalDateTime> dates;
private final Random random = new Random();
private final DayGatherer gatherer = new DayGatherer();
public GathererBenchmark() {
    int maxSize = 100_000;
    dates = IntStream.range(0, maxSize).mapToObj(_ -> LocalDateTime.of(2020 + random.nextInt(2),
            random.nextInt(12) + 1, random.nextInt(28) + 1, random.nextInt(24),
            random.nextInt(60), 0)).sorted().collect(Collectors.toList());
}

Результати benchmarks:

Benchmark                                           Mode  Cnt                 Score                Error  Units
GathererBenchmark.useGatherers    avgt    5       1078655.828   188602.966  ns/op
GathererBenchmark.useStreamsAPI  avgt    5      1749030.674      84104.040  ns/op

Тут Stream Gatherers показав ще більш разючий результат, обійшовши звичаний варіант на 40%. Загалом можна сказати, що ця фіча не тільки спрощує роботу зі Streams API, але й допомагає перевикористовувати загальні блоки трансформації коду, роблячи це ефективніше, ніж наявні підходи.

Переходимо на JDK 22

Тепер спробуємо в нашому проєкті перейти на JDK 22. Мета такої міграції — це спробувати використовувати стабільні фічі і перевірити сумісність технологій, що використовуються на проєкті Java. Почнемо із версії Java. Замінимо її для Gradle-конфігурації:

   java {
       sourceCompatibility = org.gradle.api.JavaVersion.VERSION_22
       targetCompatibility = org.gradle.api.JavaVersion.VERSION_22
   }  

Тепер запустимо тести, щоб перевірити, що наша програма працює коректно на Java 22. Відразу ж падають кілька тестів там, де ми використовуємо Mockito:

Caused by: java.lang.IllegalArgumentException: Java 22 (66) is not supported by the current version of Byte Buddy which officially supports Java 21 (65) - update Byte Buddy or set net.bytebuddy.experimental as a VM property
                    at net.bytebuddy.utility.OpenedClassReader.of(OpenedClassReader.java:96)
                    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:4011)

Таким чином, перехід на нову версію Java передбачає і оновлення ByteBuddy. Але ми не використовуємо ByteBuddy безпосередньо, він підтягується транзитивно через Mockito. Тому замінимо версію 5.5.0, яка чудово працювала з Java 21, на 5.12.0:

mockitoVersion=5.12.0

Тепер падають тести для сервісу, який працює на базі Jakarta EE:

java.lang.IllegalArgumentException: Unsupported class file major version 66
                    at jersey.repackaged.org.objectweb.asm.ClassReader.<init>(ClassReader.java:199)
                    at jersey.repackaged.org.objectweb.asm.ClassReader.<init>(ClassReader.java:180)
                    at jersey.repackaged.org.objectweb.asm.ClassReader.<init>(ClassReader.java:166)
                    at org.glassfish.jersey.server.internal.scanning.AnnotationAcceptingListener$ClassReaderWrapper.accept(AnnotationAcceptingListener.java:327)

Проблема в тому, що поточна версія Jersey (3.1.1), який використовується для REST API, містить бібліотеку Asm, яка не підтримує Java 22. Оскільки Asm вмикається не транзитивно, а як repackaged, то єдиний спосіб це виправити — оновити Jersey до 3.1.7:

val jerseyVersion = "3.1.7"

Тепер не відбувається компіляція у сервісі, заснованому на Micronaut:

Compilation failure
[ERROR] Unable to implement Repository method: PaymentRepository.delete(Object entity). Cannot query persistentEntity [Payment] on non-existent property: entity

Повідомлення про помилку виглядає не зовсім зрозуміло, але враховуючи досвід виправлення в інших сервісах, спробуємо просто перейти на останню версію Micronaut:

plugins {
    id("io.micronaut.application") version "4.4.0"
    id("com.github.johnrengelman.shadow") version "8.1.1"
    id("io.micronaut.aot") version "4.4.0"      
}
 
micronaut {
   version.set("4.5.0")

Тепер під час компіляції виникає нова помилка:

Compilation failure
[ERROR] Unable to implement Repository method: PaymentProviderRepository.findById(Object id). Cannot query entity [PaymentProvider] on non-existent property: Id

Такі самі помилки виводяться всім полям наших сутностей, які використовуються у запитах. Зараз ми використовуємо Lombok для генерації гетерів/сетерів:

@Getter
@Setter
public abstract class BaseEntity {
      
       @Id
       @GeneratedValue
       private ObjectId id;
      
       private String createdBy;
      
       @DateCreated
       private LocalDateTime createdAt;
      
       @DateUpdated
       private LocalDateTime lastModifiedAt;
}

Більше того, використовуємо останню версію Lombok (1.18.32), яка чудово працює з JDK 22. Спробуємо явно створити гетери/сетери для цих полів:

@Getter
@Setter
public abstract class BaseEntity {
      
       @Id
       @GeneratedValue
       private ObjectId id;
      
       private String createdBy;
      
       @DateCreated
       private LocalDateTime createdAt;
      
       @DateUpdated
       private LocalDateTime lastModifiedAt;
 
       public ObjectId getId() {
              return id;
       }
 
       public void setId(ObjectId id) {
              this.id = id;
       }
 
       public String getCreatedBy() {
              return createdBy;
       }
 
       public void setCreatedBy(String createdBy) {
              this.createdBy = createdBy;
       }
}

Компіляція минає без проблем, але падають тести у цьому ж сервісі:

payment.config.MongoConfigTest.loadMongoDatabaseName_validConfig_success(MongoConfig) -- Time elapsed: 0.163 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <local> but was: <null>

З якоїсь причини не проходить тест, який перевіряє валідацію для нашої Mongo-конфігурації:

@MicronautTest(rebuildContext = true)
public class MongoConfigTest {
 
       @Inject
       BeanContext beanContext;
      
       @Test
       @Property(name = "app.database.name", value = "local")
       void loadMongoDatabaseName_validConfig_success(MongoConfig mongoConfig) {
              assertEquals("local", mongoConfig.getName());
       }

Чим є MongoConfig:

@Getter
@Setter
@ConfigurationProperties("app.database")
public class MongoConfig {
      
       @NotBlank
       @NotEmpty 
       private String name;
      
       private Map<String, String> collections;
 
}

Тут також використовуються Lombok-анотації, спробуємо згенерувати гетери/сетери:

@ConfigurationProperties("app.database")
public class MongoConfig {
      
       @NotBlank
       @NotEmpty 
       private String name;
      
       private Map<String, String> collections;
 
       public String getName() {
              return name;
       }
 
       public void setName(String name) {
              this.name = name;
       }
 
       public Map<String, String> getCollections() {
              return collections;
       }
 
       public void setCollections(Map<String, String> collections) {
              this.collections = collections;
       }
}

Тепер тести відбуваються без будь-яких проблем, що свідчить: вже у Micronaut є проблеми у зв’язці з Lombok і JDK 22.

Тепер запускаємо Gradle збірку, яке падає з помилкою для Spring Boot сервісів:

Execution failed for task ': user-service:resolveMainClassName'.
> Unsupported class file major version 66

Ця проблема добре відома, і для її вирішення потрібно перейти з поточної версії Spring Boot 3.2.0 на 3.2.6:

plugins {
     id("org.springframework.boot") version "3.2.6" apply true
     java
}
 
val springBootVersion = "3.2.6"

Тепер компіляція, запуск тестів та збірка всіх проєктів відбувається успішно. Залишилося запустити наші застосунки на базі Docker-контейнерів. Про те, як це відбувається, я розповім в третій частині.

Висновки

Якщо підбити підсумки першої частини міграції, вона вийшла досить тривалою, оскільки довелося витратити багато часу аналіз і дослідження помилок. Були виявлені проблеми в сумісності деяких технологій з JDK 22, деякі з яких вдалося виправити простим оновленням, для деяких доводилося вигадувати обхідні шляхи (workarounds). У третій частині цієї статті я продовжу свою розповідь про міграцію, про нові проблеми, з якими ми зіткнулися, і які з нових фіч нам вдалося застосувати.

Якщо ще раз повернутися до benchmarks, то Stream Gatherers показали дуже хороші результати швидкодії в порівнянні з традиційним Streams API, тож чекаємо їхньої фінальної версії.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі