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.
@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 работают по одной схеме: клиент держит не сам бин, а прокси контейнера. Каждый вызов проходит через цепочку перехватчиков контейнера.
- Клиент вызывает метод прокси, полученного через
@EJB,@Injectили JNDI. - Удалённый вызов: аргументы сериализуются и передаются по сети. Локальный вызов: передаются по ссылке.
- Безопасность: контейнер проверяет роль вызывающего. Нет прав -
EJBAccessException, метод не вызывается. - Транзакция: по атрибуту метода контейнер открывает новую, входит в текущую или приостанавливает её.
- Экземпляр: для stateless и MDB берётся свободный из пула, для stateful - экземпляр этого клиента. Для singleton
берётся единственный экземпляр с учётом
@Lock. - Вызывается метод бина и пользовательские перехватчики (
@AroundInvoke). - На выходе контейнер коммитит или откатывает транзакцию, возвращает экземпляр в пул, отдаёт результат клиенту.
Итого: транзакция, безопасность и пул - это шаги одного вызова через прокси. Поэтому вызов метода изнутри
самого бина через 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.