И снова здравствуйте! 🙂
На этот раз я выбрал общую задачу, которая, по моему мнению, в большинстве случаев выполняется неправильно: отправка электронной почты. Не то чтобы люди не могли понять, как работают API электронной почты, такие как JavaMail или Apache Commons-Email . Обычно я вижу проблему в том, что они недооценивают необходимость сделать процедуру отправки почты асинхронной и что она также должна запускаться только при успешной фиксации базовой транзакции (большую часть времени).
Подумайте об общем случае использования, когда пользователь совершает покупки в Интернете. Когда он закончит, он, вероятно, захочет получить электронное письмо с подтверждением заказа. Процесс оформления заказа довольно сложен: мы обычно вставляем записи во множество различных таблиц, а также, возможно, удаляем записи для удаления товаров со склада и так далее. Все это, конечно, должно быть выполнено в одной атомарной транзакции:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
//A sample EJB method //(using CMT for transaction management) public void saveOrder() { //saving some products entityManager.persist(product1); entityManager.persist(product2); //removing them from stock entityManager.remove(product1); //and at last, we have to send that email sendOrderConfirmationMail(); //the transaction has not yet been commited by this point } |
Как и в случае с псевдокодом выше, мы обычно стремимся не допустить логику транзакций в наш код. То есть мы используем CMT (транзакции, управляемые контейнером), чтобы контейнер делал все за нас и сохранял наш код чище. Таким образом, сразу после того, как наш вызов метода завершен, контейнер EJB фиксирует нашу транзакцию. Это проблема номер 1: когда вызывается метод sendOrderConfirmationMail () , мы не можем знать, будет ли транзакция успешной. Пользователь может получить подтверждение для несуществующего заказа.
Если это то, что вы еще не поняли, просто запустите тестирование в любом из ваших кодов. Эти вызовы entityManager.persist () не запускают никаких команд базы данных до тех пор, пока не завершится вызов метода. Просто поставьте точку останова и убедитесь сами. Я видел такие беспорядки много раз.
Поэтому в случае отката нам не нужно отправлять письма. Все может пойти не так по ряду причин: сбой системы, некоторые бизнес-правила могут отказать в покупке, проверка кредитной карты и т. Д.
Итак, мы уже знаем, что при использовании CMT нам может быть трудно узнать, успешна ли транзакция или нет. Следующая проблема — сделать процедуру рассылки асинхронной, полностью независимой от нашей процедуры заказа. Представьте себе, что, если все идет хорошо с процессом заказа, но возникает какое-то исключение при попытке отправить электронное письмо? Должны ли мы откатить все только потому, что наше письмо-подтверждение не может быть отправлено? Должны ли мы действительно запретить пользователю покупать в нашем магазине только потому, что у нашего почтового сервера плохой день?
Я знаю, что подобные бизнес-требования могут идти в любом направлении, но также следует помнить, что обычно желательно, чтобы внутренняя задержка отправки почты не мешала обработке заказа. В большинстве случаев обработка заказа является нашей главной целью. Задачи с низким приоритетом, такие как отправка электронных писем, могут даже быть отложены на время, когда нагрузка на сервер низкая.
Вот так
Чтобы решить эту проблему, я выбрал подход чистой Java EE. Не нужно использовать сторонние API. Наша среда включает в себя:
- JDK 7 или выше.
- Java EE 7 (JBoss Wildfly 8.1.0)
- CDI 1.1
- EJB 3.2
- JavaMail 1.5
Я создал небольшой веб-проект, чтобы вы могли видеть, как все работает, скачайте его здесь, если хотите .
Прежде чем углубляться в код, просто кратко рассмотрим следующее: решение, показанное ниже, состоит в основном из событий CDI, смешанных с асинхронными вызовами EJB. Это потому, что спецификация CDI 1.1 не обеспечивает асинхронную обработку событий. Кажется, это что-то обсуждается для спецификации CDI 2.0, все еще в разработке. По этой причине чистый подход CDI может быть сложным. Я не говорю, что это невозможно, я просто даже не пытался.
Пример кода — это лишь пример для варианта использования «Зарегистрировать клиента». Куда мы отправим письмо для подтверждения регистрации пользователя. Общая архитектура выглядит примерно так:
Пример кода также представляет собой «тестовый случай неудачи», так что вы можете увидеть, что при откате письмо не отправляется. Я только показываю вам «счастливый путь», начиная с Managed Bean, вызывающего наш CustomerService EJB. Ничего интересного, только шаблонный
Внутри нашего CustomerService EJB все становится интересным. Используя API-интерфейс CDI, мы запускаем событие MailEvent в конце метода saveSuccess () :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Stateless public class CustomerService { @Inject private EntityManager em; @Inject private Event<MailEvent> eventProducer; public void saveSuccess() { Customer c1 = new Customer(); c1.setId(1L); c1.setName( "John Doe" ); em.persist(c1); sendEmail(); } private void sendEmail() { MailEvent event = new MailEvent(); event.setSubject( "Async email testing" ); event.setMessage( "Testing email" ); eventProducer.fire(event); //firing event! } } |
Класс MailEvent — это обычный POJO, представляющий наше событие. Он содержит информацию о сообщении электронной почты: получателе, теме, текстовом сообщении и т. Д .:
1
2
3
4
5
6
7
|
public class MailEvent { private String to; //recipient address private String message; private String subject; //getters and setters } |
Если вы новичок в CDI и все еще немного озадачены этим событием, просто прочитайте документы . Это должно дать вам идею.
Затем пришло время для наблюдателя событий, EJB MailService . Это простой EJB с некоторой магией JavaMail и парочкой аннотаций, на которые вы должны обратить внимание:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Singleton public class MailService { @Inject private Session mailSession; //more on this later @Asynchronous @Lock (LockType.READ) public void sendMail( @Observes (during = TransactionPhase.AFTER_SUCCESS) MailEvent event) { try { MimeMessage m = new MimeMessage(mailSession); Address[] to = new InternetAddress[] { new InternetAddress(event.getTo())}; m.setRecipients(Message.RecipientType.TO, to); m.setSubject(event.getSubject()); m.setSentDate( new java.util.Date()); m.setContent(event.getMessage(), "text/plain" ); Transport.send(m); } catch (MessagingException e) { throw new RuntimeException(e); } } } |
Как я уже сказал, это обычный EJB. Что делает этот класс наблюдателем событий, точнее, методом sendMail () , так это аннотацией @Observe в строке 9. Одна только эта аннотация заставит этот метод работать после запуска события.
Но нам нужно, чтобы это событие было запущено только при совершении транзакции ! Откат не должен вызывать электронную почту. Вот тут и вступает атрибут «while». Указывая значение TransactionPhase.AFTER_SUCCESS, мы гарантируем, что событие инициируется только в случае успешной фиксации транзакции.
И последнее, но не менее важное: нам также нужно заставить эту логику работать в отдельном потоке от нашей основной логики. Он должен работать асинхронно. И для этого мы просто использовали две аннотации EJB, @Asynchronous и @Lock (LockType.READ) . Последний, @Lock (LockType.READ) не требуется, но настоятельно рекомендуется. Это гарантирует, что блокировки не используются, и несколько потоков могут использовать метод одновременно.
Настройка почтового сеанса в JBoss Wildfly 8.1.0
В качестве бонуса я покажу, как мы можем правильно настроить «источник» почты в JBoss WildFly. Почтовые источники во многом похожи на источники данных, за исключением того, что они предназначены для отправки электронной почты, а не для базы данных :). Это способ отделить код от соединения с почтовым сервером. Я использовал соединение с моей учетной записью Gmail, но вы можете переключиться на что угодно, не трогая код внутри класса MailService .
Объект javax.mail.Session можно получить по его имени JNDI с помощью аннотации @Resource :
1
2
|
@Resource (mappedName = "java:jboss/mail/Gmail" ) private Session mailSession; |
Вы, наверное, заметили, что в моих предыдущих фрагментах кода я не использовал аннотацию @Resource , я использовал только @Inject CDI. Ну, если вам интересно, как я это сделал, просто скачайте исходный код и посмотрите. ( подсказка: я использовал класс помощника продюсера .)
Двигаясь дальше, просто откройте standalone.xml (или domain.xml, если вы находитесь в режиме домена) и сначала найдите «почтовую подсистему». Это должно выглядеть так:
1
2
3
4
5
|
< subsystem xmlns = "urn:jboss:domain:mail:2.0" > < mail-session name = "default" jndi-name = "java:jboss/mail/Default" > < smtp-server outbound-socket-binding-ref = "mail-smtp" /> </ mail-session > </ subsystem > |
По умолчанию почтовый сеанс уже запущен на localhost. Поскольку на ваших машинах разработки у нас, вероятно, нет почтовых серверов, мы добавим новый, указывающий на gmail:
1
2
3
4
5
6
7
8
|
< subsystem xmlns = "urn:jboss:domain:mail:2.0" > < mail-session name = "default" jndi-name = "java:jboss/mail/Default" > < smtp-server outbound-socket-binding-ref = "mail-smtp" /> </ mail-session > < smtp-server outbound-socket-binding-ref = "mail-gmail" ssl = "true" username = "[email protected]" password = "your-password" /> </ mail-session > </ subsystem > |
Посмотрите, как строки 5, 6 и 7 выделены. Это наш новый почтовый сеанс. Но это не все. Нам все еще нужно создать привязку сокета к нашему новому почтовому сеансу. Поэтому внутри standalone.xml ищите элемент с именем socket-binding-group :
1
2
3
4
5
6
7
8
9
|
< socket-binding-group name = "standard-sockets" default-interface = "public" port-offset = "${jboss.socket.binding.port-offset:0}" > <!-- a bunch of stuff here --> < outbound-socket-binding name = "mail-smtp" > < remote-destination host = "localhost" port = "25" /> </ outbound-socket-binding > </ socket-binding-group > |
Теперь мы добавляем наш порт gmail к существующим, создавая новый элемент привязки исходящих сокетов :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
< socket-binding-group name = "standard-sockets" default-interface = "public" port-offset = "${jboss.socket.binding.port-offset:0}" > <!-- a bunch of stuff here --> < outbound-socket-binding name = "mail-smtp" > < remote-destination host = "localhost" port = "25" /> </ outbound-socket-binding > <!-- "mail-gmail" is the same name we used in the mail-session config --> < outbound-socket-binding name = "mail-gmail" > < remote-destination host = "smtp.gmail.com" port = "465" /> </ outbound-socket-binding > </ socket-binding-group > |
Это оно. Пожалуйста, оставьте комментарий, если у вас есть какие-либо вопросы :). Позже!
Ссылка: | CDI & EJB: отправка асинхронной почты об успешной транзакции от нашего партнера JCG Родриго Учоа из Code to live. Живи к коду. блог. |