Як мігрувати проєкт з JDK 17 на JDK 21

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

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

Що нового в JDK 21

Нещодавно вийшла нова LTS-версія Java-платформи — JDK 21. Я вже багато разів розповідав про ті фічі, які з’явилися в JDK, починаючи з JDK 18, але ми не могли їх використовувати, оскільки, по-перше, це були версії із короткостроковою підтримкою. По-друге, вони перебували в режимі ознайомлення (preview), їхня функціональність була нестабільною і могла змінюватися. І ось нарешті багато з них визнані стабільними та готові для використання на production. Перерахуємо такі фічі:

  1. Record patterns.
  2. Pattern matching for switch.
  3. Віртуальні потоки.

Така відносно велика кількість фіч, пов’язаних з Java Records, свідчить про те, що інженери Oracle серйозно просувають цю функціональність. І якщо у Java 8 дуже багато було зроблено для просування функціонального програмування (Streams API), то у Java 17/21 є нахил на immutability, що лежить в основі Records.

Деякі фічі з JDK 21 досі перебувають у режимі preview:

  1. Scoped values.
  2. Structures concurrency.

Ну і в цьому релізі з’явилися нові фічі, про які ми, власне, і поговоримо трохи далі:

  1. Строкові темплейти.
  2. Безіменні патерни та змінні.
  3. Безіменні класи та main-методи.
  4. Послідовні колекції (Sequenced Collections).

Перші три поки що в режимі preview, а четверта визнана стабільною. З неї і почнемо.

Sequenced collections

Уже в першій версії Java були свої колекції (Vector, Stack і Hashtable), які ніяк не залучали інтерфейси при своєму оголошенні. Ця помилка була врахована в Java 1.2, коли з’явився Collections framework і такі базові інтерфейси, як Iterable, Collection, List і Set, причому:

  • List — список упорядкованих елементів;
  • Set — набір унікальних елементів.

Щоправда, вже тоді з’явилися перші питання про такий поділ функціональності і про те, чому, наприклад, колекція не може бути і впорядкованою, і не містити дублікатів (така, до речі, з’явилася у JDK 1.4 — LinkedHashSet). Усе було пояснено особливостями дизайну, і на цьому дискусія закінчилася. Але проблема лишилася. Усі ми знаємо про принцип Liskov substitution. Якщо ми оголосимо такий метод:

       private void handle (List<User> users) {

то зможемо туди передати будь-який об’єкт-список або завантажити точніше будь-який об’єкт з властивостями списку, наприклад ArrayList або LinkedList. Але як нам оголосити метод, який зможе приймати як аргумент будь-яку колекцію з упорядкованим зберіганням елементів? Це неможливо. Більш того, поточний SDK від Java також не покращить становище. Наприклад, якщо у нас був об’єкт LinkedHashSet, і ми хотіли перетворити його на immutable:

       private void handle (LinkedHashSet<User> users) {
              Set<User> allUsers= Collections.unmodifiableSet(users);

то отримували об’єкт типу Set. Тобто ознака впорядкованості губилася. Ситуація почала потихеньку змінюватися після виходу JDK 17. У 2021 з’явилася перша пропозиція створити упорядкований тип, який назвали ReversibleCollection (альтернативна назва — OrderedCollection). І ось у JDK 21 з’явилося розширення Collections Framework та новий тип — SequencedCollection. Це інтерфейс, який визначає будь-яку колекцію, де є впорядкованість елементів і де можна чітко виділити перший і останній елемент. Зараз у ній шість методів:

  • reversed();
  • addFirst();
  • addLast();
  • getFirst();
  • getLast();
  • removeFirst();
  • removeLast();

Серед спадкоємців нового інтерфейсу — List, Deque та ще один новий тип SequencedSet. SequencedSet — це колекція, яка поєднує унікальність і впорядкованість (TreeSet або LinkedHashSet). Але як тоді працюватиме цей код, якщо ми не можемо самі позиціонувати елементи у TreeSet:

              TreeSet<String> set = new TreeSet<>();
              set.addFirst("1");

Усе дуже просто, він буде викидати UnsupportedOperationException. Виникає питання, а чи потрібні нам одночасно List і SequencedCollection, якщо насправді List це і є SequencedCollection? Інженери Oracle підійшли до цього питання досить практично, зробивши SequencedCollection максимально легковагим і додавши туди лише методи, пов’язані з доступом до окремих елементів. Але частина SDK усе ж була оновлена. Наприклад, з’явився новий метод, який дозволить писати коректніший код:

private void handle (LinkedHashSet<User> users) {
       SequencedSet<User> allUsers= Collections.unmodifiableSequencedSet(users);

Unnamed variables

Наступна фіча (поки що в режимі тестування) — безіменні змінні (unnamed variables). Думаю, що в кожного розробника в роботі траплялися ситуації, коли доводилося оголошувати змінні, які ніде більше не використовувалися. Наприклад, порожній блок catch:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {        
}

Тепер ви можете використовувати замість назви (ідентифікатора) змінної просто символ підкреслення:

try {
    Thread.sleep(1000);
} catch (InterruptedException _) {
}

Водночас підкреслення, починаючи з Java 9, було зарезервованим словом (у Java 8 видавалося попередження).

Здавалося б, яка різниця, назвати змінну _ чи e? В обох випадках це один символ, ми особливо нічого не заощаджуємо. Але у разі підкреслення мається на увазі, що це не просто змінна, а змінна, яка більше ніяк не використовується, що підвищує читабельність коду. Використовувати такі назви можна в обмеженій кількості випадків і оголосити таке поле не вийде (помилка компіляції):

private int _ = 1;

Ще одне обмеження — ви не можете використовувати такі змінні далі в коді:

int _ = 1;
System.out.println(_);

Другий рядок викликає компілятор здивування та помилку: <i>Using '_' as a reference is not allowed</i>. Це призводить до кумедних прикладів, наприклад, можна оголосити кілька змінних з формально однаковим ідентифікатором:

int _ = 1;
int _ = 2;

Також не можна використовувати такі змінні, як назви аргументів:

public class Starter {
    public static void main(String[] _) {

А ось де варто їх застосовувати, то це різні pattern matching, наприклад для instanceof. Якщо в цьому прикладі вам не важливе ім’я людини:

private void handle(Object info) {
    if(info instanceof UserInfo(int id, String name)) {

то можна знову замінити його підкресленням:

private void handle(Object info) {
    if(info instanceof UserInfo(int id, String _)) {

Unnamed classes і instance main methods

Будь-яка мова програмування мріє про збільшення популярності. Цього можна досягти, наприклад, зниженням вхідного порогу навчання. У Java 9 з’явилася утиліта JShell, яка дозволяла писати код у спрощеному стилі та інтерактивному режимі. А в JDK 21 з’явилася фіча — безіменні класи та main-методи.

Скільки б у вас не було досвіду з Java, ви хоч раз писали такий клас:

public class Starter {
    public static void main(String[] args) {        
    }
}

І, швидше за все, не використовували аргумент args, та й не завжди розуміли, чому метод main повинен бути static. Для того, щоб новачкам було простіше вивчати Java, було вирішено послабити вимоги до такого класу, що запускається, і тепер написати main() набагато простіше:

public class Starter {
    void main() {
    }
}

Головне, щоб метод main був не приватним. Цей код можна ще більше спростити, якщо викинути оголошення класу:

void main() {
}

Такий синтаксис добре знайомий Kotlin-розробникам. Але ж у JVM методи не можуть існувати без класів. Клас нікуди не зник, і він буде згенерований з таким ім’ям, як і назва файлу.

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

String templates

Четверта фіча — string templates. Практично будь-яка сучасна мова програмування підтримує інтерполяцію рядків. Наприклад, у Kotlin ви можете написати:

val name = "John"
val text = "His name is $name"

У JavaScript так само можна зробити за допомогою косих лапок:

let name = "John"
let text = `His name is $name`

Не можна сказати, що в Java зовсім немає можливості для інтерполяції. Вищевказаний приклад можна переписати як:

String name = "John";
String text = "His name is %s".formatted(name);

Але такий код більш громіздкий і менш читабельний. Тому Java-розробники давно вимагали реалізувати інтерполяцію, і в JDK 21 з’явилися string templates. Ключова їхня роль відводиться інтерфейсам StringTemplate і StringTemplate.Processor.

Processor (або template processor) відповідає за валідацію та трансформацію, тоді як StringTemplate зберігає деталі вашого темплейту. Зараз у JDK є три процесори (RAW, STR та FMT), які неявно імпортуються в кожному Java-файлі. І наш приклад можна переписати як:

String name = "John";
StringTemplate template = RAW."His name is \{name}";
String text = template.interpolate();

RAW — це процесор, який бере рядок-темплейт та створює на його основі об’єкт StringTemplate, де рядкові фрагменти відокремлені від виразів (у нашому випадку \{name}). А далі вже на основі поточних значень виразів обчислюється підсумковий рядок. Але така форма ще більш громіздка, тому її можна переписати як:

String name = "John";
String text = STR."His name is \{name}";

Тут STR — це процесор, який виконує всю чорнову роботу і повертає обчислений рядок. Що можна вказувати як template expressions? Можна викликати методи:

public static String getName() {
    return "John";
}
String text = STR."His name is \{getName()}";

Викликати методи у об’єктів:

String name = "John";
String text = STR."His name is \{name.toUpperCase()}";

Що, якщо потрібно викликати метод класу іншого пакета? Не біда, його можна проімпортувати прямо в рядку:

String text = STR. "His name is \{demo.model.Storage.getName()}" ;

Водночас новий формат \{} вже не можна використовувати без template processor (буде помилка Processor missing from string template expression):

String text = "His name is \{name}";

Що якщо ми намагаємося використати змінну, що не існує? Зрозуміло, отримаємо помилку компіляції:

Cannot resolve symbol 'name2'

А якщо name буде null? Тоді null і буде підставлено замість name. Але що, якщо ми викличемо метод у null-об’єкта?

String name = null;
String text = STR. "His name is \{name.toUpperCase()}" ;

Тут ми отримаємо стандартний NullPointerException.

Якщо у вас legacy-проєкт, то, можливо, ви часто використовуєте метод String.format і не хочете витрачати час на переписування коду під string templates. На щастя, вже зараз є процесор FormatProcessor (доступний як FMT), який підтримує таке форматування:

int amount = 100;
String text = FMT. "The amount is %x\{amount}";

Тут на виході ми отримуємо The amount is 64. Останнє питання — наскільки швидко це все працює? Створимо невеликий benchmark на базі JMH 1.37:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
public class StringTemplates {
    private String name = "John";
    @Benchmark
    public String stringConcatenation() {
        return "My name is" + name;
    }
    @Benchmark
    public String stringFormat() {
        return String.format("My name is %s", name);
    }
    @Benchmark
    public String stringBuilder() {
        return new StringBuilder("My name is %s").append(name).toString();
    }
    @Benchmark
    public String stringTemplates() {
        return STR. "My name is \{ name }" ;
    }

Тут ми тестуємо чотири способи конкатенації рядків. Результати тестування показують, що String templates виконуються так само швидко, як звичайне об’єднання рядків і поступаються StringBuilder всього на 10%. Для порівняння — String.format працює у вісім разів повільніше.

Benchmark                            Mode  Cnt   Score   Error  Units
StringTemplates.stringBuilder        avgt    5   9.632 ± 0.042  ns/op
StringTemplates.stringConcatenation  avgt    5  10.389 ± 0.351  ns/op
StringTemplates.stringFormat         avgt    5  83.597 ± 0.620  ns/op
StringTemplates.stringTemplates      avgt    5  10.373 ± 0.027  ns/op

Яким способом забезпечується така ефективність? Якщо подивитися на декомпільований байт-код:

int amount = 100;
String text = "The amount is %x" + amount;

то видно, що основну частину роботи виконує компілятор, який шукає в рядку вирази та замінює їх на звичайну конкатенацію.

Починаємо міграцію

Що використовувати з розібраних фіч? Поки що не варто використовувати preview-фічі (вони можуть змінитися в наступних версіях). Перш за все, зверніть увагу на ті ж Java Records. Вони з’явилися ще в JDK 16, але тільки недавно Java-бібліотеки / -фреймворки почали їх підтримувати, тому ми можемо їх безпечно застосувати. Наприклад, у нас є клас Address:

@Embeddable
public class Address {
       private String zipCode;
      
       private String street;
      
       private String houseNo;
 
       private String apartment;
 
       @Column(name = "ZIP_CODE", length = 10)
       public String getZipCode() {
              return zipCode;
       }

Його можна переробити в запис, оскільки Hibernate підтримує їх, починаючи з версії 6.0. Щоправда доведеться додати спеціальний клас Instantiator і вказати його в анотації @EmbeddableInstatiator. Це не дуже зручно, і у версії 6.2 така анотація (і окремий клас) більше не потрібні. У нашому проєкті ми використовували Hibernate 6.1.7, тож перейдемо на 6.2.0:

val hibernateVersion = "6.2.0.Final"

Перепишемо Address як:

@Embeddable
public record Address(@Column(name = "ZIP_CODE", length = 10) String zipCode,
       @Column(name = "STREET", length = 32) String street, @Column(name = "HOUSE_NO", length = 16) String houseNo, @Column(name = "APARTMENT", length = 16) String apartment) {
 }

Головний side effect від Java records — перехід від гетерів (getStreet</i>()) до read accessors (street()). Тобто вам доведеться переписати код, де вони використовуються. Відносний мінус Java records — потрібно під час створення об’єкта вказувати всі поля, навіть опціональні:

station.setAddress(new Address(dto.getApartment(), dto.getHouseNo(), dto.getStreet(), dto.getZipCode()));

Запускаємо тести, усі тести відбуваються успішно. Ще один тип, який можна змінити — Coordinate:

@Embeddable
@Setter @NoArgsConstructor @AllArgsConstructor
public class Coordinate {
       private double x;
      
       private double y;

на:

@Embeddable
public record Coordinate(@Column(name = "X") double x, @Column(name = "Y") double y) {}

Наступний кандидат у записі — RangeCriteria:

@Getter
public class RangeCriteria {
       private final int page;
 
       private final int rowCount;
 
       public RangeCriteria(final int page, final int rowCount) {
              Checks.checkParameter(page >= 0, "Incorrect page index:" + page);
              Checks.checkParameter(rowCount >= 0, "Incorrect row count:" + rowCount);
 
              this.page = page;
              this.rowCount = rowCount;
       }
}

Тут все трохи складніше, тому що потрібно перевіряти вхідні параметри в конструкторі, спрощеним записом не обійдешся. Тому доведеться використовувати звичайний конструктор для перевірки:

public record RangeCriteria(int page, int rowCount) {
 
       public RangeCriteria {
              Checks.checkParameter(page >= 0, "Incorrect page index:" + page);
              Checks.checkParameter(rowCount >= 0, "Incorrect row count:" + rowCount);
       }
}

Зверніть увагу на два важливі нюанси:

  1. Конструктор не має дужок.
  2. У конструкторі не проводиться привласнення полів (це виконується автоматично).

Ще один претендент на записі — різноманітні DTO, наприклад CreateTripDTO:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CreateTripDTO {
      
       private int routeId;
      
       private LocalDate date;
 
}

Досить легко переписати як:

public record CreateTripDTO (int routeId, LocalDate date) {}

Тести показують, що Java Records чудово інтегровані і з Jackson, і Spring MVC. Але що, якщо у вас не Jackson для серіалізації, а Gson? Майже три роки йшли розмови про підтримку записів у ньому, і нарешті у версії 2.10.1, яка вийшла в січні цього року, така підтримка з’явилася.

Якщо у вас Spring Boot 3.x проєкт і ви використовуєте Configuration Properties, то можете для mapping застосовувати не тільки класи, але і Java records. У нас для цього є клас RouteDefinition:

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class RouteDefinition {
      
       private String host;
      
       private int port;
}

який замінимо на:

public record RouteDefinition(String host, int port) {}

Перейдемо до віртуальних потоків. Чи можемо ми їх уже зараз використати? Так, цілком, щоправда поки в ручному виконанні. А ось у версії Spring Boot 3.2, яка скоро вийде, додано нову властивість spring.threads.virtual.enabled. Якщо його встановити в true, то Spring автоматично перетворить всі ExecutorService/TaskExecutor з platform-threaded на virtual-threaded. Ще один проєкт, який почав їх підтримувати — Tomcat. У 11 версії (яка ще бета) вже є підтримка пула потоків (Executor), заснованого на віртуальних потоках — org.apache.catalina.core.StandardVirtualThreadExecutor. Також у версії Micronaut 4.x, якщо ви використовуєте JDK19+, можна переключитися з реактивного стека на блокуючий (але на основі віртуальних потоків)

Ще одна фіча, яку давно хотілося спробувати — запечатані типи. У нас є невеликий компонент для генерації випадкових рядків. Для того, щоб ніхто не намагався успадкувати його типи (Composition over inheritance), оголосимо базовий інтерфейс як sealed та вкажемо після permits ті класи, які можуть його реалізовувати:

public sealed interface NumberGenerator
              permits RandomNumberGenerator, SecureRandomNumberGenerator, SequentialNumberGenerator {
       int generate();
 }

Правда, тепер потрібно змінити кожен клас і вказати там або модифікатор final, sealed або non-sealed:

public final class RandomNumberGenerator implements NumberGenerator {

Так, запечатані типи дозволяють створити ізольовану ієрархію типів без можливості їх успадкування ззовні (а лише використання).

Тепер спробуємо перейти на JDK 21:

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

Збираємо проєкт і одразу отримуємо помилку:

Fatal error compiling: java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field ’com.sun.tools.javac.tree.JCTree qualid’

Ця проблема добре відома і, на щастя, має рішення перейти на останню версію Lombok (1.18.30).

Продовжуємо складання та отримуємо помилку при запуску тестів:

Underlying exception : org.mockito.exceptions.base.MockitoException: Could not modify all classes [interface java.util.function.Function]

at common.infra.util.SecurityUtilTest.getUserId_idExists_success(SecurityUtilTest.java:42)

Caused by: org.mockito.exceptions.base.MockitoException: Could not modify all classes [interface java.util.function.Function]

at common.infra.util.SecurityUtilTest.getUserId_idExists_success(SecurityUtilTest.java:42)

Caused by: java.lang.IllegalStateException:

Byte Buddy could not instrument all classes within the mock’s type hierarchy

Зараз ми послуговуємося Mockito 5.2.0, яке транзитивно використовує ByteBuddy 1.14.1. ByteBuddy не має проблем з підтримкою JDK 21, тому спробуємо просто оновитися до останньої версії Mockito (5.5.0). Продовжуємо збiрку та отримуємо ще одну помилку при запуску тестів у JAX-RS сервісі:

Mockito cannot mock this class: interface persistence.loader.EntityLoader.

Underlying exception : org.mockito.exceptions.base.MockitoException: Could not modify all classes [interface persistence.loader.EntityLoader]

Caused by: org.mockito.exceptions.base.MockitoException: Could not modify all classes [interface persistence.loader.EntityLoader]

Caused by: java.lang.IllegalStateException:

Byte Buddy could not instrument all classes within the mock’s type hierarchy

Ця помилка — копія попередньої, хоч версію Mockito ми оновили. Проблема в тому, що ByteBuddy транзитивно завантажується з Hibernate Core, причому Hibernate 6.2 використовує досить стару його версію. Тому перейдемо на останню версію Hibernate (6.3.1):

val hibernateVersion = "6.3.1.Final"

Тепер оновимо версію JDK у Dockerfile скриптах. Почнемо із Gradle images. Цікава ситуація зі Spring Boot додатками. Ми зараз використовуємо збiрку Eclipse Temurin від Adoptium:

FROM eclipse-temurin:17-jre-alpine

Але досі на Adoptium немає релізу для JDK 21. На їхньому сайті, як і раніше, JDK 17 — це остання випущена LTS. А в розділі нічних релізів останнє збирання для JDK 21 випущено більше місяця тому і позначено як EA (Early Access). Пояснення не дає ясності, коли цю збірку чекати — " We are awaiting access to the new Java 21’s specification tests before formally releasing Temurin 21 «

Чи є альтернативи? Так, є ось список дистрибутивів, які вже підтримують JDK 21:

  1. Amazon Corretto
  2. Azul Zulu
  3. Liberica

Правда, тільки Liberica надає JRE версію для JDK 21. Спробуємо використовувати її як експеримент, тим більше вона добре відома і стабільна:

 FROM bellsoft/liberica-openjre-alpine:21

Потім з’являється другий сюрприз. Поточна версія Gradle 8.3 ще не сумісна з JDK 21, ця підтримка буде реалізована тільки у версії 8.4. Якщо запустити збирання на JDK 20:

FROM gradle:8-jdk20-alpine as gradle

то передбачувано отримаємо помилку:

Invalid source release: 21

На щастя, вже є бета-версія Gradle 8.4 RC1, але, на жаль, Gradle Docker image немає «нічних» збірок для releases candidates. Ми вимушені піти іншим шляхом — взяти офіційний Dockerfile для JDK 20 Alpine і змінити в ньому рядки:

FROM eclipse-temurin:20-jdk-alpine

на

FROM bellsoft/liberica-openjdk-alpine:21

та

ENV GRADLE_VERSION 8.3

на

ENV GRADLE_VERSION 8.4-rc-1

Зберемо Docker image для локального використання:

docker build -t gradle:8-jdk21-alpine .

Залишилося тільки оновити Docker images для Tomcat (тут теж чомусь немає JRE-версії для Java 21):

FROM tomcat:jdk21-openjdk-slim

Запускаємо збірку та отримуємо нову помилку — Docker не може знайти на Docker Hub im-age gradle: 8-jdk21-alpine. Його й справді там немає, але він є у локальному репозиторії. Детальне дослідження цієї проблеми показало, що збiрка безпосередньо через docker build проходить успішно, а ось через docker compose build немає. Це відома проблема, для якої немає якогось універсального рішення. Але все впирається в так званий Docker builder (або buildx). Зараз ми використовуємо builder з назвою docker-container, при переході на де-фолтну помилку зникає:

docker buildx use default

Усі сервіси запускаються та працюють успішно, щоправда в JAX-RS сервісі в логах дивне попередження, яке потребує додаткового дослідження:

WARNING [main] org.glassfish.jersey.server.internal.scanning.AnnotationAcceptingListener$ClassReaderWrapper.accept Unsupported class file major version 65

Висновки

Можна сказати, що загалом усі бібліотеки та фреймворки виявилися готовими до виходу JDK 21, у деяких випадках (Lombok, ByteBuddy) потрібно лише оновитися до останніх версій. А ось хто виявився неготовим, то системи збирання (Gradle) і організації, що випускають JDK-дистрибутиви (Adoptium). Але ще гірше від того, що в них немає якогось розпису і неможливо передбачити, коли вони випустять версії, засновані на JDK 21. Зрозуміло, вам не варто писати власний Dockerfile, як я описав у статті, а просто дочекатися виходу Gradle 8.4, який скоро буде.

Поки писалася ця стаття, 4 жовтня таки вийшла версія Gradle 8.4 (але не Eclipse Temurin 21). І це призвело до цікавої колізії. Gradle 8.4 підтримує JDK 21, але Gradle Docker image заснований на Eclipse Temurin, тому в офіційних images все ще використовується JDK 20, а це означає, що використовувати їх для збирання не можна.

Водночас дуже добре, що ми живемо в часи, коли є не одне джерело JDK-дистрибутивів (як раніше був Oracle JDK) — ми маємо кілька альтернатив, і, наприклад, Liberica вже надає Docker images з усіма останніми змінами.

З нових фіч найбільш затребувані Java Records та pattern matching. Також цікаво спробувати віртуальні потоки, коли вийде Spring Boot 3.2 (або за умови використання Micronaut 4).

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному2
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

Что то не похоже, что код становится более простым и читабельным.

"

Другий рядок викликає компілятор здивування

"
«Компілятор здивування» — це якась нова фіча JDK 21 ?

Так, компілятор здивування йде на доу, і перевіряє чи написав Сергій ще одну статтю з серії «як написать хеловорлд» і якщо не знаходить — повідомляє систему про здивування.

мене цікавить питання конкретно поставлене «як мігрувати». Чи правильно я розумію що ні якої реальної «міграції» не відбувається у випадку коли ми просто перезапускаємо старий вже існуючий код з 17 на 21? А може й з 11 на 21? Тобто код змінювати не доведеться а тільки білд скріпти так?

А у контексті статті «міграція» мається на увазі одразу же ж застосування нових фіч нової версії так?

Якщо останнє питання «так» тоді теж питання а загалом це популярна практика одразу же ж використовувати нові фічі нової версії саме на етапі «міграції» на нову версію?

«Новый, улучшеный, теперь с банановым вкусом!» Ну кое-что полезно конечно, но ничего такого, что открывало бы принципиально новые возможности.

Поки писалася ця стаття, вийшли Gradle 8.4 та Eclipse Temurin 21, тому зараз можемо використовувати в наших Dockerfile:

FROM eclipse-temurin:21-jre-alpine
FROM gradle:8-jdk21-alpine

Єдине обмеження для JDK 21 — Gradle 8.4 не підтримує скрипти збирання на Kotlin, якщо вони використовують плагін kotlin-dsl.

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