Перейти к содержанию

Бины в Jakarta EE

Бин - Это объект, которым управляет контейнер: создаёт его, внедряет зависимости, ведёт жизненный цикл. В Jakarta EE несколько «семейств» управляемых компонентов, у каждого свой контейнер и своя роль.

Note

Три похожих названия - три разные вещи: - JavaBean - соглашение о том, как написан класс. Контейнер им не управляет - Enterprise JavaBean (EJB) - компонент EJB-контейнера: транзакции, пул, таймеры (см. EJB) - CDI-бин (managed bean) - объект CDI-контейнера, основная модель бинов в Jakarta EE

Отдельный контейнер со своими бинами - у Spring.


JavaBean

JavaBean - Это обычный Java-класс, который следует соглашениям, чтобы инструменты могли работать с его свойствами по именам: JSP-теги, EL, JSF, сериализация. - Конструктор по умолчанию - публичный, без параметров - Приватные свойства - все поля private - Геттеры и сеттеры - публичные getName(), setName(), для boolean - isActive() - Сериализация - может реализовывать Serializable, чтобы сохранять состояние, например в сессии

public class User implements Serializable {
    private String name;
    private int age;

    public User() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

См. JavaBeans в JSP.


CDI-бины

CDI-бин (Contexts and Dependency Injection) - Это класс, которым управляет CDI-контейнер: внедрение зависимостей, скоупы, квалификаторы, события, продюсеры, перехватчики, декораторы. Лёгкий и гибкий, сейчас это основной выбор. - любой POJO становится CDI-бином, если у него есть bean-defining аннотация (обычно scope) и он лежит в bean archive (см. beans.xml) - клиент получает не сам объект, а прокси. Бин с @ApplicationScoped создаётся при первом вызове метода

import jakarta.enterprise.context.ApplicationScoped;   // scope CDI
import jakarta.inject.Inject;                          // внедрение CDI

@ApplicationScoped              // делает класс CDI-бином, один экземпляр на всё приложение
public class VehicleService {

    @Inject
    VehicleDao vehicleDao;      // DAO - тоже CDI-бин

    public void createVehicle(VehicleDto dto) {
        // бизнес-логика
    }
}

Транзакции на CDI-бине включаются @Transactional, где его ставить - в Где ставить @Transactional.


Скоупы

Жизненный цикл CDI-бина определяет scope:

Scope Живёт Для чего
@ApplicationScoped Всё время работы приложения, один экземпляр Сервисы, DAO, hub'ы рассылки
@RequestScoped Один HTTP-запрос Контроллеры и ресурсы, которым нужно состояние на время запроса
@SessionScoped Одна HTTP-сессия Состояние пользователя в веб-приложении, используют редко
@ConversationScoped Управляемая «длинная» беседа, несколько запросов Сценарии из JSF, используют редко
@Dependent Столько же, сколько бин-владелец. Скоуп по умолчанию, своего контекста нет Использовать осознанно: у каждого владельца свой экземпляр
new → @PostConstruct (при первом использовании в своём scope) → рабочее состояние → @PreDestroy (конец scope)

Возможности CDI

  • Внедрение: @Inject в поля, конструкторы, сеттеры
  • Имя для EL и JSF: @Named("name"), без имени - имя класса с маленькой буквы
  • Квалификаторы: @Qualifier + свои аннотации, чтобы различать реализации (например @PrimaryDb)
  • Альтернативы: @Alternative - подменить реализацию, например на заглушку в тестах
  • Продюсеры: @Produces создаёт бин или ресурс, @Disposes закрывает его
  • Стереотипы: @Stereotype собирает набор аннотаций в одну
  • Перехватчики: @Interceptor + @AroundInvoke или @AroundConstruct, включаются через @Priority или beans.xml
  • Декораторы: @Decorator оборачивает вызовы бина и добавляет поведение
  • События: Event<T> + @Observes, асинхронно - @ObservesAsync
  • Расширения: portable extensions (SPI) - менять и добавлять бины при старте контейнера
  • Транзакции: @Transactional из jakarta.transaction (см. JTA)
  • Безопасность: Jakarta Security или средства сервера, на методах те же @RolesAllowed

beans.xml и обнаружение бинов

beans.xml - Это файл настройки CDI для архива: какие классы считать бинами и что включить. - в WAR лежит в WEB-INF/beans.xml, в JAR - в META-INF/beans.xml. В Maven-проекте это src/main/webapp/WEB-INF/beans.xml или src/main/resources/META-INF/beans.xml - с CDI 1.1 (Java EE 7) CDI включён и без beans.xml: архив считается implicit bean archive, и бинами становятся классы с bean-defining аннотациями. Это требование спецификации, а не особенность WildFly или Payara

Режимы обнаружения (bean-discovery-mode): - annotated - бины только классы с bean-defining аннотациями: scope, @Dependent, @Interceptor, @Decorator, стереотип - all - бин любой подходящий класс, даже без аннотаций - none - CDI для архива выключен

<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
       version="4.0" bean-discovery-mode="annotated">
    <alternatives>
        <class>com.example.MockPaymentGateway</class>   <!-- включить альтернативу -->
    </alternatives>
</beans>

Когда beans.xml нужен: - бинами должны стать классы без аннотаций - режим all - включить @Alternative, если у неё нет @Priority - задать порядок перехватчиков и декораторов, если у них нет @Priority - исключить классы из сканирования - <scan><exclude> - выключить CDI для архива - режим none

Когда не нужен: - бины помечены scope-аннотациями и особых настроек нет. Так устроено большинство проектов - в проекте нет CDI, например только сервлеты

Warning

Пустой beans.xml до CDI 4.0 означал режим all. С CDI 4.0 (Jakarta EE 10) пустой файл - это annotated. При переезде на Jakarta EE 10 классы без аннотаций перестают быть бинами.

Note

В исходной версии было «без beans.xml контейнер не будет обрабатывать @Inject» и «beans.xml нужен, чтобы интегрировать CDI с EJB и JAX-RS». Так было до Java EE 7. Сейчас CDI работает без файла, а для интеграции с EJB и JAX-RS он не нужен.


EJB и другие управляемые компоненты

EJB - бины EJB-контейнера: @Stateless, @Stateful, @Singleton, @MessageDriven. Дают «из коробки» транзакции, пул, таймеры, асинхронность, безопасность, удалённые вызовы. Session bean - одновременно и CDI-бин: в нём работают @Inject и @Observes.

Остальные компоненты, которыми управляет контейнер:

Компонент Аннотации и интерфейсы Когда применять
JAX-RS ресурс @Path, @GET, @POST, @PUT, @DELETE, @Consumes, @Produces REST API для фронтенда или интеграции, например /api/vehicles. С CDI это CDI-бин, по умолчанию живёт один запрос
Servlet @WebServlet Низкоуровневый HTTP, если не хватает JAX-RS. Например, отдача бинарных файлов (см. Java Servlets)
Filter @WebFilter Перехват запросов и ответов: CORS, логирование, аутентификация
Listener @WebListener События контейнера: создание и удаление сессий, запуск и остановка приложения
WebSocket endpoint @ServerEndpoint Реалтайм: чат, уведомления, обновление таблиц в UI. Внутрь можно внедрять CDI- и EJB-бины
JSF / MVC бин @Named, @ViewScoped, @SessionScoped, @RequestScoped Данные прямо в представление: #{bean.property}. Это обычный CDI-бин с @Named
Batch @BatchProperty, ItemReader, ItemWriter, Job и Step в XML Массовая обработка данных: ETL, миграции, отчёты
JCA connector @Connector, @ActivationSpec Интеграция с внешними системами через Resource Adapter, напрямую редко

JSF Managed Beans - @ManagedBean и скоупы из jakarta.faces.bean.*. Удалены в Jakarta Faces 4.0, вместо них CDI + @Named и JSF-скоупы (@ViewScoped из jakarta.faces.view) или обычные CDI-скоупы. Как их писали раньше - в JSF.


Что бинами не является

  • DTO - обычный POJO для входа и выхода API. Это транспорт, контейнер им не управляет
  • JPA Entities - @Entity, @Embeddable, @MappedSuperclass. Ими управляет EntityManager, а не CDI. Только данные и простая логика (см. JPA)
  • Bean Validation - не вид бина, а аннотации-ограничения @NotNull, @Size, @Positive, @Pattern, @Email. Ставятся на поля DTO, сущностей и параметры. Проверку запускают JAX-RS, JPA перед сохранением и CDI
  • Утилиты - чистые функции и константы: final класс, приватный конструктор, только статические методы (или Lombok @UtilityClass). Внедрять нечего, состояния нет

CDI или EJB

Оба - управляемые контейнером объекты, но с разной задачей. CDI отвечает за DI, жизненный цикл и контексты. EJB даёт готовый enterprise-набор: транзакции, безопасность, таймеры, распределённость.

CDI EJB
Сколько экземпляров Задано scope, скоупы гибкие Задано контрактом, три варианта: пул, на клиента, один
Транзакции Только с @Transactional (см. JTA) Каждый метод по умолчанию REQUIRED
Конкурентность Не управляется, @ApplicationScoped должен быть потокобезопасным сам Один вызов на экземпляр, @Lock у singleton
Пул и пассивация Нет. @SessionScoped сериализуется вместе с сессией Есть
Таймеры, асинхронность Через Jakarta Concurrency @Schedule, @Asynchronous
Безопасность Jakarta Security или перехватчики Встроена: @RolesAllowed, @PermitAll
Удалённый доступ Нет @Remote, сейчас почти не используют
События @Observes, @ObservesAsync Тоже есть: session bean - это ещё и CDI-бин
Вес Лёгкие, современный стандарт Тяжелее и жёстче, исторический багаж «старого монолита»

Как делают сейчас (Jakarta EE 10+) - CDI-first: сервисы, DAO, фильтры - CDI-бины. EJB берут точечно, когда нужна их специфика: - таймеры - @Singleton + @Schedule - асинхронные вызовы - @Asynchronous, хотя есть Jakarta Concurrency и ManagedExecutorService - MDB, если используется старое JMS API - remote-вызовы между JVM

Note

В исходной версии у EJB в событиях стоял «❌», а среди причин брать EJB были «асинхронные методы с retry». Session bean - это ещё и CDI-бин, поэтому @Observes в нём работает. Retry у @Asynchronous нет.

Итого: нужны сервисы, DAO, DI и транзакции - CDI + @Transactional. Нужны таймеры, асинхронность, декларативная безопасность, пул - EJB. Тренд - CDI-first: возможности EJB переезжают в CDI и отдельные спецификации, например @Transactional из JTA и @Asynchronous из Jakarta Concurrency.


Какой бин для какого слоя

Что делает каждый слой:

Слой Пакет Что делает Чего там быть не должно
Представление controller JAX-RS ресурсы, иногда MVC или JSF. Принять HTTP-запрос, провалидировать вход (Bean Validation на DTO), разобрать параметры, пагинацию, фильтры, вызвать сервис, собрать ответ, статус, заголовки Бизнес-логики и доступа к БД
Представление ws WebSocket endpoints и hub. Получить событие от сервиса и разослать подписчикам: всем или выборочно Работы с БД и бизнес-правил
Бизнес-логика service Use-case-сервисы и фасады: бизнес-правила, граница транзакции, координация нескольких DAO и внешних интеграций, публикация событий (например, wsHub.broadcast(...) после сохранения) JPA-кода: Criteria, HQL, EntityManager - это DAO
Доступ к данным dao Репозитории над JPA и Hibernate: CRUD, запросы (Criteria, HQL, native, хранимые процедуры), пагинация. Возвращают сущности или узкие проекции Бизнес-правил
Доступ к данным model JPA-сущности и value objects (@Embeddable): маппинг на таблицы, базовые инварианты данных, @PrePersist Логики, которой нужна инфраструктура: сеть, файлы, DAO. Лёгкие доменные методы можно
Контракты dto Вход и выход API: срезы полей для UI и интеграций, валидация входа, без ленивых связей Логики
Сквозное filter JAX-RS ContainerRequestFilter и ContainerResponseFilter, servlet-фильтры: аутентификация, авторизация, трейсинг, CORS, аудит, логирование, метрики Бизнес-логики
Сквозное util Чистые утилиты: форматтеры и парсеры дат, конвертеры, константы Состояния и зависимостей

В сервисе уместно: @Transactional, проверка бизнес-инвариантов, идемпотентность, блокировки (optimistic и pessimistic, см. JTA), доменные ошибки, которые ExceptionMapper переводит в HTTP-ответ.

Дополнительные пакеты по необходимости: - exception - доменные исключения и @Provider ExceptionMapper, который отдаёт единый JSON-ответ об ошибке - config - CDI-продюсеры конфигурации, настройки в @ApplicationScoped-бинах - events и interceptors - CDI-события для отложенных реакций, перехватчики для сквозной логики - mapper - преобразование Entity ⇄ DTO вручную или через MapStruct

util лучше дробить по доменам: util.time, util.json, util.aggrid. Иначе он превращается в свалку.


Каким бином сделать каждый слой:

Слой Тип Основные аннотации Транзакции
controller CDI-бин @Path, @RequestScoped (или @ApplicationScoped, если состояния нет), @Produces, @Consumes, @Inject Нет
ws Endpoint + CDI-hub @ServerEndpoint + @ApplicationScoped hub Нет
service CDI-бин @ApplicationScoped (или @Dependent), @Transactional, @Inject Да, здесь граница
dao CDI-бин @ApplicationScoped, @PersistenceContext Участвует в транзакции сервиса
model Не бин, JPA @Entity, @Embeddable -
dto POJO Bean Validation: @NotNull, @Size -
filter JAX-RS провайдер, CDI @Provider, @Priority, @Inject Нет
util Не бин final + приватный конструктор или @UtilityClass -

См. Где ставить @Transactional.

controller - ресурсы JAX-RS по спецификации совместимы с CDI. Бизнес-логика живёт вне контроллера.

@RequestScoped
@Path("/vehicle")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class VehicleController {

    @Inject
    VehicleService service;
    // endpoints...
}

ws - в endpoint'е логика приёма и отправки. Общую рассылку и хранение Session держат в отдельном CDI-синглтоне.

@ApplicationScoped
public class VehicleWsHub {
    // хранение сессий, broadcast(...)
}

@ServerEndpoint("/ws/vehicle")
public class VehicleEndpoint {

    @Inject
    VehicleWsHub hub;   // CDI-внедрение в WebSocket endpoint работает в современных серверах
    // onOpen, onMessage, onClose - делегируют hub
}

service - пример выше, в CDI-бины.

dao - инкапсулирует JPA и Hibernate. Транзакцию открывает сервис, DAO в ней участвует. Пустой результат DAO возвращает как Optional (см. Запросы в DAO и Optional).

@ApplicationScoped
public class VehicleDao {

    @PersistenceContext(unitName = "studsPU")
    EntityManager em;

    public Vehicle save(Vehicle v) {
        if (v.getId() == null) {
            em.persist(v);
            return v;
        }
        return em.merge(v);
    }
}

Note

Ещё 5-7 лет назад DAO было принято делать @Stateless EJB ради автоматических транзакций. Сейчас, когда есть @Transactional для CDI, чаще выбирают CDI + @Transactional на сервисе.

filter - сквозная инфраструктура: логирование, аутентификация и т.д.

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthFilter implements ContainerRequestFilter {

    @Inject
    AuthService auth;   // CDI

    @Override
    public void filter(ContainerRequestContext ctx) {
        // проверить токен
    }
}

Итого: CDI - по умолчанию для всего, что внедряется: контроллеры, сервисы, DAO, фильтры, hub'ы. EJB - только ради их специфики. DTO, сущности и утилиты - не бины. Так получается CDI-центричная архитектура Jakarta EE: EJB по необходимости, а не по привычке.