Міграція застосунків на 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, тож чекаємо їхньої фінальної версії.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів