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

EJB

EJB (Enterprise Java Beans) - спецификация Jakarta EE для серверной бизнес-логики, которая работает внутри контейнера сервера приложений. EJB - упакованный бэкенд-сервис с кучей инфраструктуры из коробки.

Зачем: вынести бизнес-логику в отдельный слой, отдельно от JSF, Servlet и т.п. Пишутся обычные классы с аннотациями, а контейнер EJB: - создаёт и удаляет объекты, раздаёт их из пула - включает транзакции - проверяет безопасность - даёт удалённый доступ (RMI, HTTP и т.п.) - обрабатывает сообщения (JMS) - запускает задачи по таймеру

См. Сервисы контейнера.


Типы EJB

Session beans вызываются клиентом синхронно: @Stateless, @Stateful, @Singleton. Message-driven bean работает асинхронно: его никто не вызывает, он реагирует на сообщения из брокера.

Общее для всех типов: - клиент видит прокси, а не сам объект (см. Как контейнер обслуживает вызов) - колбэки жизненного цикла: @PostConstruct после создания и внедрения зависимостей, @PreDestroy перед удалением


@Stateless

@Stateless - Это бин без состояния между вызовами. Контейнер держит пул экземпляров и отдаёт любой свободный на любой запрос. - подходит для типичных сервисов: UserService, OrderService - нет гарантии, что два вызова одного клиента попадут в один экземпляр, поэтому поля для состояния клиента бесполезны - контейнер держит от 0 до N экземпляров и сам увеличивает или уменьшает пул

@Stateless
public class OrderService {

    @PersistenceContext
    private EntityManager em;

    public void place(Order order) {   // по умолчанию транзакция REQUIRED
        em.persist(order);
    }
}

Жизненный цикл: 1) Does Not Exist - бина ещё нет. 2) Pooled (Ready) - контейнер создал объект, вызвал @PostConstruct, положил в пул. 3) Method Invocation - пришёл вызов, контейнер берёт любой экземпляр из пула, оборачивает в транзакцию и проверку безопасности, вызывает метод и возвращает экземпляр в пул. 4) Removed - при остановке приложения или сокращении пула контейнер вызывает @PreDestroy и удаляет бин.

Does Not Exist → (create) → @PostConstruct → Pooled → (method call) → Pooled → ... → @PreDestroy → Removed

@Stateful

@Stateful - Это бин, который хранит состояние между вызовами для одного клиента, как сессия. Один клиент - один экземпляр. - нужен, когда логика зависит от последовательности шагов: wizard, корзина - клиент привязан к своему экземпляру, пока не вызовет @Remove или не истечёт таймаут - пула «анонимных» экземпляров нет, каждый бин привязан к своему клиентскому диалогу - чтобы не держать все экземпляры в памяти, контейнер может их пассивировать

@Stateful
@StatefulTimeout(value = 30, unit = TimeUnit.MINUTES)
public class CartBean {

    private final List<Item> items = new ArrayList<>();   // состояние конкретного клиента

    public void add(Item item) {
        items.add(item);
    }

    @Remove   // после вызова контейнер удаляет бин
    public void checkout() {
        // оформить заказ
    }
}

Жизненный цикл: 1) Does Not Exist 2) Method-Ready (Active) - создан, прошёл @PostConstruct, обслуживает вызовы своего клиента. 3) Passive (опционально) - контейнер «усыпляет» бин: - вызывает @PrePassivate - сериализует состояние на диск или в память - когда бин снова нужен, десериализует его и вызывает @PostActivate 4) Removed - после метода с @Remove, по таймауту или при остановке приложения. Перед удалением вызывается @PreDestroy.

Does Not Exist → (create) → @PostConstruct → Active → [@PrePassivate → Passive → @PostActivate → Active]*
    → @PreDestroy → Removed

Note

Если таймаут истёк, пока бин пассивирован, контейнер может удалить его без вызова @PreDestroy. Освобождать ресурсы только в @PreDestroy для stateful ненадёжно.


@Singleton

@Singleton - Это один экземпляр на всё приложение. - для кешей, глобальных конфигов, счётчиков, реестров - @Startup - создать при запуске приложения, иначе контейнер создаст при первом обращении - конкурентность по умолчанию управляет контейнер: @ConcurrencyManagement(CONTAINER). Вариант BEAN - сам синхронизируешь код - блокировки на уровне методов: @Lock(READ) - параллельно, @Lock(WRITE) - эксклюзивно - по умолчанию все методы WRITE: вызовы идут строго по одному, и синглтон становится узким местом - @AccessTimeout - сколько ждать блокировку, потом ConcurrentAccessTimeoutException

@Singleton
@Startup
public class RatesCache {

    private Map<String, BigDecimal> rates;

    @PostConstruct
    void load() {
        rates = loadFromDb();
    }

    @Lock(LockType.READ)   // читать могут параллельно
    public BigDecimal get(String currency) {
        return rates.get(currency);
    }

    @Lock(LockType.WRITE)  // перезагрузка эксклюзивно
    @AccessTimeout(value = 5, unit = TimeUnit.SECONDS)
    public void reload() {
        rates = loadFromDb();
    }
}

Жизненный цикл: 1) Does Not Exist 2) Initialized - создаётся один раз, вызывается @PostConstruct. 3) Method-Ready - обслуживает запросы. 4) Removed - при остановке приложения, перед этим @PreDestroy.

Does Not Exist → @PostConstruct → Method-Ready → @PreDestroy → Removed

@MessageDriven

Message-Driven Bean (MDB) - Это слушатель очереди или топика JMS. Клиент его не вызывает, сообщения доставляет контейнер из брокера.

@MessageDriven(activationConfig = {
        @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/OrdersQueue"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "jakarta.jms.Queue")
})
public class OrderListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        // обработать заказ из очереди
    }
}

Жизненный цикл похож на stateless: 1) Does Not Exist 2) Pooled - несколько экземпляров, каждый прошёл @PostConstruct. 3) Message Delivery - пришло сообщение, контейнер берёт бин из пула и вызывает onMessage(). Сообщения обрабатываются параллельно, по одному на экземпляр. 4) Removed - при остановке или сокращении пула, перед этим @PreDestroy.


Итого по типам:

Тип Состояние Экземпляры Кто вызывает Для чего
@Stateless Нет Пул Клиент, синхронно Сервисы
@Stateful На клиента Один на клиента, пассивация Клиент, синхронно Wizard, корзина
@Singleton Общее Один на приложение Клиент, синхронно, @Lock Кеш, конфиг, реестр
@MessageDriven Нет Пул Брокер, асинхронно Обработка сообщений JMS

Как контейнер обслуживает вызов

Все сервисы EJB работают по одной схеме: клиент держит не сам бин, а прокси контейнера. Каждый вызов проходит через цепочку перехватчиков контейнера.

  1. Клиент вызывает метод прокси, полученного через @EJB, @Inject или JNDI.
  2. Удалённый вызов: аргументы сериализуются и передаются по сети. Локальный вызов: передаются по ссылке.
  3. Безопасность: контейнер проверяет роль вызывающего. Нет прав - EJBAccessException, метод не вызывается.
  4. Транзакция: по атрибуту метода контейнер открывает новую, входит в текущую или приостанавливает её.
  5. Экземпляр: для stateless и MDB берётся свободный из пула, для stateful - экземпляр этого клиента. Для singleton берётся единственный экземпляр с учётом @Lock.
  6. Вызывается метод бина и пользовательские перехватчики (@AroundInvoke).
  7. На выходе контейнер коммитит или откатывает транзакцию, возвращает экземпляр в пул, отдаёт результат клиенту.

Итого: транзакция, безопасность и пул - это шаги одного вызова через прокси. Поэтому вызов метода изнутри самого бина через this эти шаги пропускает.


Сервисы контейнера

Транзакции

Container-Managed Transactions (CMT) - режим по умолчанию: контейнер сам начинает, коммитит и откатывает транзакцию. - каждый бизнес-метод по умолчанию идёт с атрибутом REQUIRED - атрибут задаётся @TransactionAttribute(REQUIRED | REQUIRES_NEW | MANDATORY | SUPPORTS | NOT_SUPPORTED | NEVER). - откатить без исключения - SessionContext.setRollbackOnly()

Bean-Managed Transactions (BMT) - @TransactionManagement(BEAN): границы транзакции задаёшь сам через UserTransaction (см. JTA).

Исключения и откат: - системное исключение (RuntimeException) - откат, экземпляр бина выбрасывается, клиент получает EJBException - прикладное исключение (checked) - по умолчанию commit, клиент получает исключение как есть - @ApplicationException(rollback = true) - прикладное исключение, которое откатывает транзакцию

@Stateless
public class PaymentService {

    @Resource
    private SessionContext ctx;

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)   // своя транзакция
    public void pay(Payment payment) throws PaymentDeclinedException {
        if (!payment.isValid()) {
            ctx.setRollbackOnly();   // откатить, не бросая исключение
        }
    }
}

@ApplicationException(rollback = true)   // checked, но откатывает транзакцию
public class PaymentDeclinedException extends Exception {
}

Безопасность

Контейнер проверяет права до входа в метод. Пользователя и его роли знает сервер: они приходят из логина (веб-модуль, Jakarta Security) и передаются в вызов EJB. - @RolesAllowed("admin") - только для ролей - @PermitAll - всем, @DenyAll - никому - @DeclareRoles - объявить роли, которые проверяются в коде - @RunAs("system") - бин вызывает другие бины от имени указанной роли - в коде: ctx.getCallerPrincipal() и ctx.isCallerInRole("admin")

@Stateless
@DeclareRoles({"admin", "user"})
public class AdminService {

    @Resource
    private SessionContext ctx;

    @RolesAllowed("admin")
    public void deleteUser(long id) {
        String who = ctx.getCallerPrincipal().getName();   // кто вызвал
    }

    @PermitAll
    public String status() {
        return "ok";
    }
}

Пул экземпляров

Пул - контейнер заранее создаёт экземпляры @Stateless и MDB и переиспользует их между вызовами. - один экземпляр в момент времени обслуживает один вызов, поэтому код бина потокобезопасен без synchronized - размер пула ограничивает параллелизм: все экземпляры заняты - следующий вызов ждёт - пул настраивается в сервере, а не аннотацией

Note

Спецификация разрешает пул, но не обязывает. В WildFly пул для @Stateless по умолчанию выключен: экземпляр создаётся на вызов. Для MDB пул есть.


Local и Remote

Способ вызова бина задаётся интерфейсом. - @Local - вызов внутри того же приложения и JVM. Аргументы передаются по ссылке, вызов дешёвый - @Remote - вызов через сеть из другого приложения или сервера (RMI-IIOP или протокол сервера). Аргументы сериализуются, поэтому передаются по значению и должны быть Serializable - No-interface view (@LocalBean или класс без интерфейсов) - локальный вызов напрямую по классу

@Local
public interface OrderServiceLocal {
    void place(Order order);
}

@Remote
public interface OrderServiceRemote {
    void place(OrderDto order);   // DTO, потому что уходит по сети
}

@Stateless
public class OrderService implements OrderServiceLocal, OrderServiceRemote {
    // ...
}

Как получить: - локально - @EJB или @Inject - удалённо - lookup в JNDI: java:global/<app>/<module>/OrderService!com.example.OrderServiceRemote

Изменение объекта-аргумента внутри @Remote-метода клиент не увидит: он передал копию.


Таймеры

Таймер - планировщик задач в контейнере. - декларативно: @Schedule с cron-подобными полями, несколько расписаний - @Schedules - программно: TimerService.createTimer(...), метод-обработчик помечается @Timeout - по умолчанию таймер persistent = true: сервер сохраняет его, и пропущенные срабатывания выполнятся после рестарта

@Singleton
public class ReportJob {

    @Schedule(hour = "*", minute = "0", second = "0", persistent = false)   // каждый час
    public void hourly() {
        // построить отчёт
    }
}

Асинхронность и JMS

@Asynchronous - метод выполняется в отдельном потоке контейнера, клиент сразу получает управление. - возвращает void или Future<V> (результат оборачивают в AsyncResult)

@Stateless
public class ReportService {

    @Asynchronous
    public Future<Report> build(long id) {
        Report report = generate(id);
        return new AsyncResult<>(report);
    }
}

JMS - асинхронный обмен через брокер: отправитель кладёт сообщение в очередь, MDB его обрабатывает (см. @MessageDriven). Отправитель не ждёт обработки и не знает, кто её сделает.


Шпаргалка аннотаций EJB:

Что Аннотации
Типы @Stateless, @Stateful, @Singleton, @MessageDriven
Интерфейсы доступа @Local, @Remote, @LocalBean
Транзакции @TransactionAttribute, @TransactionManagement (BEAN или CONTAINER), @ApplicationException
Безопасность @RolesAllowed, @PermitAll, @DenyAll, @DeclareRoles, @RunAs
Асинхронность @Asynchronous
Таймеры @Schedule, @Schedules, @Timeout
Жизненный цикл @PostConstruct, @PreDestroy, для @Stateful - @Remove, @PrePassivate, @PostActivate
Конкурентность @Singleton @Lock (READ или WRITE), @AccessTimeout, @ConcurrencyManagement

EJB и CDI

Session bean - одновременно и CDI-бин. См. CDI или EJB.