Що приніс довгоочікуваний реліз Jakarta EE 11. Мігруємо правильно

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

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник, хочу поділитися з вами своїм досвідом міграції проєктів на Jakarta EE 11, яка вийшла цього року. Взагалі кажучи, її вихід на планувався на 2024 рік, але через різні проблеми затримка склала більше року. Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про нові фічі в Jakarta EE 11, розібратися в тому, як провести міграцію застосунків на нову версію і які тут можуть бути складності.

Нові фічі та зміни

Вихід довгострокового релізу JDK 25 змусив Java розробників на якийсь час забути про те, що цього року влітку вийшов давно очікуваний реліз Enterprise Java — Jakarta EE 11. Він вийшов 26 червня 2025 року. Сам реліз є досить складним процесом, оскільки зараз у Jakarta EE виділяють три основні групи специфікації:

  1. Core Profile — мінімальний набір специфікацій для створення вебзастосунків, що включає різні анотації, JAX-RS, CDI та роботу з JSON (вийшла в грудні 2024-го року)
  2. Web Profile — специфікації для створення вебзастосунків(вийшов 30 березня 2025 року)
  3. Platform — вона і вийшла 26 червня

Розбиття всієї платформи на профілі дуже зручне для розробників, тому що їм не потрібно чекати остаточного релізу, якщо вони використовують лише Core або Web Profile. Згідно з опитуванням Jakarta EE Developer Survey 2025, на момент релізу всієї платформи Jakarta EE 11, 18% ре-спондентів вже використовували ті чи інші її специфікації.

Наскільки привабливим Java розробників виглядає цей реліз? Коли я два роки тому писав про Jakarta EE 10, то вже тоді автори Enterprise Java заявляли, що у наступній версії ключовими будуть три нові специфікації:

  1. Jakarta Config
  2. Jakarta RPC
  3. Jakarta Data

Jakarta Data 1.0 дійсно потрапила до поточної версії, я про неї вже писав, тому тут особливо розповідати про неї не буду. Jakarta Config все ще допрацьовується, причому не сказати, що активно, оскільки останній коміт по ній був більше року тому, а поточна версія 0.1 каже, що її майбутнє туманне. Про Jakarta RPC теж не можна сказати нічого певного, останній коміт у неї був понад два роки тому.

Що ж увійшло до нової версії?

  1. Мінімально підтримувана версія тепер JDK 17. Це дозволило додати підтримку Java Records для багатьох специфікацій
  2. Що стосується runtime, то тут заявлено підтримку JDK 21, що дозволить використовувати віртуальні потоки для деяких специфікацій
  3. Більше ніяк не використовується SecurityManager, який був відключений у JDK 24
  4. Деякі специфікації були видалені з Jakarta EE (Jakarta XML Binding, Jakarta XML Web Services). Це не означає, що самі специфікації/технології більше не підтримуються. Просто вони вже втратили свою актуальність, тому їх видалення дозволить вендорам швидше створювати реалізації специфікацій для повної підтримки Enterprise Java
  5. Якщо ви все ще використовуєте JSF (Jakarta Faces), то вас напевно засмутить видалення Managed Beans та їх заміна на CDI Beans, тому що доведеться змінювати всі анотації, деяку конфігурацію та перетестувати змінені модулі. Правда, тепер ви можете повноцінно використовувати ті фічі CDI, яких не було в JSF (interceptors, conversation scopes, events)

Загалом Jakarta EE 11 може похвалитися однією новою та 16 існуючими специфікаціями. Для того, щоб ознайомитися із змінами, почнемо з Core Profile:

У CDI 4.1 дві істотні зміни:

  1. Method invokers, які дозволяють розробникам CDI-based фреймворків неявно викликати клієнтський код
  2. Анотацію @Priority дозволено використовувати для producer methods, які декларують нові біни (за аналогією з @Bean у Spring Framework

У Jakarta REST (JAX-RS) 4.0 головна зміна — видалення підтримки legacy технологій, таких як Jakarta XML Binding та Managed Beans, а також підтримка PATCH методу та специфікації JSON Merge Patch для часткової зміни ресурсів.

У Jakarta Annotations 3.0 єдина зміна — це видалення інструкції @ManagedBean, яка була оголошена deprecated ще в Jakarta Faces 2.3. Замість неї пропонується використовувати інструкцію @Named. Якщо вам не хочеться вручну робити таку заміну, то вже є готовий рецепт від Open Rewrite.

У Jakarta Interceptors 2.2 головна зміна — це підтримка Jakarta Annotations 3.0. Тепер перейдемо до змін у всій платформі.

Тут з’явилася нова специфікація — Jakarta Data 1.0, яку я вже згадував. Крім того, було змінено 12 специфікацій. Розберемо найважливіші з них.

У Jakarta Persistence 3.2 додалося найбільше нових функцій. Насамперед це довгоочікувана підтримка програмної конфігурації. З перших версій JPA Java розробники писали конфігурацію в спеціальному файлі persistence.xml. Тепер же можна обійтися без нього:

EntityManagerFactory emf = new PersistenceConfiguration("UserData«)
           .managedClass(Order.class)
           .managedClass(Product.class)
           .property(PersistenceConfiguration.JDBC_URL,
           «jdbc:h2:mem:products;NON_KEYWORDS=VALUE»)
           .property(PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION,
           «update»)
                      .createEntityManagerFactory();

По суті це аналог persistence.xml, але з деякими зручностями. Наприклад, назви властивостей є константами, тому менше шансів помилитися у тому описі. Тим не менш, все ще доводиться кожен клас додавати окремо (немає підтримки додавання всього пакету, як наприклад у Spring). Тому, якщо ви точно знаєте свого JPA провайдера (Hibernate), краще використовувати розширення класу PersistenceConfiguration — HibernatePersistenceConfiguration, щоб писати більш зрозумілий і компактний код:

EntityManagerFactory emf = new HibernatePersistenceConfiguration("UserData")
           .managedClasses(Order.class, Product.class)
           .jdbcUrl("jdbc:h2:mem:products;NON_KEYWORDS=VALUE")
           .schemaToolingAction(Action.UPDATE)
           .createEntityManagerFactory();

В обох наведених варіантах відразу вказується, що потрібно створити схему даних на старті роботи застосунку. З новим Schema Manager API можна розділити процес керування DDL операціями та створення конфігурації для Jakarta Persistence:

EntityManagerFactory emf = new HibernatePersistenceConfiguration("UserData")
           .managedClasses(Order.class, Product.class)
           .jdbcUrl("jdbc:h2:mem:products;NON_KEYWORDS=VALUE«)
           .createEntityManagerFactory();
emf.getSchemaManager().create(true);
emf.getSchemaManager().drop(true);

Тобто ви можете в будь-який момент створити, видалити або провалідувати вашу схему даних, якщо у вас є доступ до поточної EntityManagerFactory. Ще одна цікава зміна — новий атрибут comment для анотацій @Table/@Column, який буде використовуватися при генерації схеми:

@Entity
@Table(name = «PRODUCTS», comment = «Basic product information»)
public class Product extends BaseEntity {

Тобто в цьому випадку ми отримаємо ще один запит:

comment on table PRODUCTS is ’Basic product information’

Ця зміна є логічною, тому що в тому ж Hibernate вже давно є анотація @Comment:

@Entity
@Table(name = «PRODUCTS»)
@Comment("Basic product information«)
public class Product extends BaseEntity {

Ще одна довгоочікувана зміна — новий метод getSingleResultOrNull:

@Override
public R getSingleResultOrNull() {
           final List<R> resultList = getResultList();
           if ( resultList == null || resultList.isEmpty() ) {
                      return null;
           }
           else if ( resultList.size() > 1 ) {
                      throw new NonUniqueResultException(
                      String.format(
                      «Call to stored procedure [%s] returned multiple results»,
                      getProcedureName()
                      ));
           }
           return resultList.get( 0 );
}

Він прийшов на зміну getSingleResult(), який має суттєвий мінус — він викидає NoResultException у разі відсутності даних у відповіді від сервера.

Інша зміна — поява маркерів-інтерфейсів FindOption, RefreshOption, LockOption:

public interface FindOption {
}

Вони були додані для того, щоб їх можна було використовувати для операцій з даними в EntityManager. Наприклад, для пошуку використовується інтерфейс FindOption:

<T> T find(Class<T> entityClass, Object primaryKey, FindOption... options);

Це дозволяє гнучкіше передавати різні опції у цей метод. Раніше можна було явно вказати лише одну опцію, наприклад, тип блокування:

<T> T find(Class<T> entityClass, Object primaryKey, LockModeType lockMode);

Зараз можна вказувати кілька комбінацій таких опцій:

var product = em.find(Product.class, 1, CacheMode.IGNORE, Timeout.s(1000));

Також можна відзначити інші зміни в Jakarta Persistence 3.2:

  1. Тепер можна Java records оголошувати, як embeddable сутності (хоча це давно вже реалізовано в Hibernate)
  2. Можливість mapping для Instant/Year типів даних/полів
  3. Додана підтримка функцій union, intersect, except, cast, left, right, and replace
  4. Тепер можна використати || для конкатенації рядків
  5. Для полів, де вказується інструкція @Version, можна використовувати типи LocalDateTime та Instant

У Jakarta Validation 3.1 (яке раніше називалося Jakarta Bean Validation), одна істотна зміна — це підтримка Java records:

record Product(@Positive int id, @NotBlank String name) {}

Таким чином, якщо ви використовуєте Hibernate Validator 8.0.1 або пізнішої версії, ця фіча вам вже доступна.

У Jakarta Concurrency 3.1 головна зміна це підтримка віртуальних потоків з JDK 21. Але оскільки Jakarta EE 11 все ще базується на JDK 17, то цю підтримку вирішили прив’язати не до API, а до конфігурації (куди додали новий атрибут virtual):

@ManagedExecutorDefinition(
           name = «java:comp/vtExecutor»,
           maxAsync = 10,
           context = «java:comp/vtContextService»,
           virtual = true
)
@ApplicationScoped
public class VirtualThreadAsyncConfig {
}

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

Якщо ви хочете перевірити Jakarta EE 11 у роботі, вже є окремий сайт start.jakarta.ee, де можна в режимі конструктора вибрати версію, профіль та інші атрибути для генерації проєкту.

Я ж у цій статті опишу досвід міграції кількох невеликих, але досить насичених технологіями проєктів.

Переходимо на Jakarta EE 11

Тепер спробуємо перейти на Jakarta EE 11, щоб протестувати та використати її фічі. Почнемо з першого проєкту, де з Enterprise Java використовується Jersey 3.1, Hibernate 6.6 та Hibernate Validator 8.0.1. Замінимо їх послідовно на Jersey 4.0 (вийшла лише листопаді цього року), Hibernate 7.1.11:

<properties>
           <jersey.version>4.0.0</jersey.version>
           <hibernate.version>7.1.11</hibernate.version>
</properties>

Та Hibernate Validator 9.1.0:

<dependency>
           <groupId>org.hibernate.validator</groupId>
           <artifactId>hibernate-validator</artifactId>
           <version>9.1.0.Final</version>
           <scope>runtime</scope>
</dependency>

Також оновимо Jakarta Validation та Jakarta Annotations залежності. Спробуємо перебрати проєкт і відразу отримуємо купу помилок компіляції. Розберемо їх по порядку.

Перша група помилок пов’язана з тим, що з інтерфейсу Session Hibernate були видалені методи, які не відповідали (за назвами) аналогічним в EntityManager — це delete і saveOrUpdate. Вони були оголошені deprecated ще в Hibernate 6.0, а зараз відповідно потрібно перейти в першому випадку на remove(), а в другому на більш складний алгоритм:

if (station.getId() == 0) {
           session.persist(station);
} else {
           session.merge(station);
}

Крім того, Hibernate 7.0 був оголошений deprecated метод get(), так що саме час замінити його на find(). Ще один метод, який не рекомендується використовувати — createNamedQuery без типу, що повертається:

session.createNamedQuery(Station.QUERY_DELETE_ALL).executeUpdate();

Але в нашому випадку ми маємо запит, який видаляє дані і нічого не повертає. Тому потрібно використовувати новий метод createNamedMutationQuery:

session.createNamedMutationQuery(Station.QUERY_DELETE_ALL).executeUpdate();

Тенденція змін дотяглася і такого типу як Interceptor. Ще в Hibernate 6.6 були оголошені deprecated методи onSave/onDelete, тому зараз рекомендується перейти на onPersist/onRemove, а крім того, тип параметра id змінений з Serializable на Object:

default boolean onPersist(Object entity, Object id, Object[] state, String[] propertyNames, Type[] types) {
           return onSave(entity, id, state, propertyNames, types);
}

Можливо, це пов’язано з глобальними змінами щодо ідентифікаторів сутностей, так як і в EntityManager у них тепер тип Object, а не Serializable.

Проблеми роботи з даними вирішені, перейдемо до веб-компонентів Jersey. Клас ComponentBinder не компілюється, тому що не знаходиться імпорт:

import org.glassfish.jersey.internal.inject.AbstractBinder;

Без нього не обійтися, тому що на ньому побудована вся робота ComponentBinder:

public class ComponentBinder extends AbstractBinder {
@Override
protected void configure() {

Виявилося, що клас просто перенесено до іншого пакету:

import org.glassfish.hk2.utilities.binding.AbstractBinder;

Тепер залишилася лише одна помилка компіляція в методі configure:

bindFactory(() -> ConfigProvider.getConfig()).to(Config.class).in(Singleton.class);

Метод bindFactory() приймає об’єкт типу Factory, який більше не функціональний інтерфейс, так що доведеться його замінити анонімним класом:

bindFactory(new Factory<Config>() {
           @Override
           public Config provide() {
                      return ConfigProvider.getConfig();
           }
           @Override
           public void dispose(Config instance) {
           }
           }).to(Config.class).in(Singleton.class);

Тепер сервіс збирається та працює без помилок. Тепер перейдемо до другого проєкту. Оновимо версію PrimeFaces:

<dependency>
           <groupId>org.primefaces</groupId>
           <artifactId>primefaces</artifactId>
           <version>15.0.10</version>
           <classifier>jakarta</classifier>
</dependency>

Jakarta Servlet API:

<dependency>
           <groupId>jakarta.servlet</groupId>
           <artifactId>jakarta.servlet-api</artifactId>
           <version>6.1.0</version>
           <scope>provided</scope>
</dependency>

CDI:

<dependency>
           <groupId>jakarta.enterprise</groupId>
           <artifactId>jakarta.enterprise.cdi-api</artifactId>
           <version>4.1.0</version>
</dependency>

Jakarta Authentication API:

<dependency>
           <groupId>jakarta.authentication</groupId>
           <artifactId>jakarta.authentication-api</artifactId>
           <version>3.1.0</version>
           <scope>provided</scope>
</dependency>

Jakarta Faces:

<dependency>
           <groupId>org.glassfish</groupId>
           <artifactId>jakarta.faces</artifactId>
           <version>4.1.4</version>
</dependency>

Soteria:

<dependency>
           <groupId>org.glassfish.soteria</groupId>
           <artifactId>jakarta.security.enterprise</artifactId>
           <version>3.0.4</version>
</dependency>

Jakarta RESTful API:

<dependency>
           <groupId>jakarta.ws.rs</groupId>
           <artifactId>jakarta.ws.rs-api</artifactId>
           <version>4.0.0</version>
</dependency>

Запускаємо збірку, і вона падає з помилкою компіляції, тому що в класі SamRegistrationIn-staller не знаходяться два імпорти:

import static org.glassfish.soteria.mechanisms.jaspic.Jaspic.deregisterServerAuthModule;
import static org.glassfish.soteria.mechanisms.jaspic.Jaspic.registerServerAuthModule;

Про цю помилку я писав ще два роки тому, на жаль, жодної відповіді на неї не було. Але якщо тоді ми просто відкотилися на минулу версію Eclipse Soteria, то зараз це зробити не вийде, оскільки нам доводиться використовувати як нову версію Jakarta Security API (3.0), так і її реалізації — Soteria (3.4). В результаті аналізу зміни в коді Soteria з’ясувалося, що тепер для реєстрації або видалення модулів можна використовувати native Jakarta Security API, що ми зробимо

Замість:

registerServerAuthModule(new HttpBridgeServerAuthModule(cdiPerRequestInitializer), context);

буде

AuthConfigFactory.getFactory()
.registerServerAuthModule(new HttpBridgeServerAuthModule(cdiPerRequestInitializer), context);

А замість

deregisterServerAuthModule(context);

Буде:

AuthConfigFactory.getFactory().removeServerAuthModule(context);

Тепер збирання та запуск усіх тестів проходить успішно. Перезбираємо Docker images, запускаємо контейнери — проєкт працює без помилок. Таким чином, ми успішно перейшли на Jakarta EE 11 платформу.

Висновки

Якщо говорити про зміни в Jakarta EE 11, то можна згадати вихід Jakarta Data та підтримка нових JDK фіч — Java records та віртуальні потоки. Як свідчить опитування Jakarta EE Developer Survey 2025, вже 58% респондентів використовують Jakarta EE (Spring Boot — 56%), але при цьому 40% все ще сидять на Jakarta EE 8 (яка по суті є копією Java EE 8, яка вийшла ще в 2017-му). Тому авторам Jakarta EE потрібно інтенсивніше впроваджувати нові фічі та випускати нові версії, щоб не втратити довіру своїх розробників.

Сам перехід на Jakarta EE 11 не викликав особливих проблем, незважаючи на безліч breaking changes в Hibernate та Jersey.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось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

Дещо стороннє питання. Як людині, що ніяк не знайома с EE, проте багато мала справи з SE, встановити простий веб-сервер з фічами на рівні раутера запитів, генератора html з шаблонів, точкою входу для REST, авторизацією і куками, не підіймаючи всю махіну EE?
Я питав і різні ШІ, вони всі щось таке рекомендують, що первісна наладка стає надто тяжкою.

А що до речі з Microprofile? Там точно Config був і декілька інших зручностей. Вони що, не поділили щось з Jakarta EE ?

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