И снова здравствуйте! 🙂
На этот раз я выбрал общую задачу, которая, по моему мнению, в большинстве случаев выполняется неправильно: отправка электронной почты. Не то чтобы люди не могли понять, как работают 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)publicvoidsaveOrder() {    //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 | @StatelesspublicclassCustomerService {        @Inject    privateEntityManager em;        @Inject    privateEvent<MailEvent> eventProducer;        publicvoidsaveSuccess() {        Customer c1 = newCustomer();        c1.setId(1L);        c1.setName("John Doe");        em.persist(c1);                sendEmail();    }    privatevoidsendEmail() {        MailEvent event = newMailEvent();        event.setTo("some.email@foo.com");        event.setSubject("Async email testing");        event.setMessage("Testing email");        eventProducer.fire(event); //firing event!    }} | 
Класс MailEvent — это обычный POJO, представляющий наше событие. Он содержит информацию о сообщении электронной почты: получателе, теме, текстовом сообщении и т. Д .:
| 1 2 3 4 5 6 7 | publicclassMailEvent {    privateString to; //recipient address    privateString message;    privateString 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 | @SingletonpublicclassMailService {        @Inject    privateSession mailSession; //more on this later        @Asynchronous    @Lock(LockType.READ)    publicvoidsendMail(@Observes(during = TransactionPhase.AFTER_SUCCESS) MailEvent event) {        try{            MimeMessage m = newMimeMessage(mailSession);            Address[] to = newInternetAddress[] {newInternetAddress(event.getTo())};            m.setRecipients(Message.RecipientType.TO, to);            m.setSubject(event.getSubject());            m.setSentDate(newjava.util.Date());            m.setContent(event.getMessage(),"text/plain");                        Transport.send(m);        } catch(MessagingException e) {            thrownewRuntimeException(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")privateSession mailSession; | 
Вы, наверное, заметили, что в моих предыдущих фрагментах кода я не использовал аннотацию @Resource , я использовал только @Inject CDI. Ну, если вам интересно, как я это сделал, просто скачайте исходный код и посмотрите. ( подсказка: я использовал класс помощника продюсера .)
Двигаясь дальше, просто откройте standalone.xml (или domain.xml, если вы находитесь в режиме домена) и сначала найдите «почтовую подсистему». Это должно выглядеть так:
| 1 2 3 4 5 | <subsystemxmlns="urn:jboss:domain:mail:2.0">    <mail-sessionname="default"jndi-name="java:jboss/mail/Default">        <smtp-serveroutbound-socket-binding-ref="mail-smtp"/>    </mail-session></subsystem> | 
По умолчанию почтовый сеанс уже запущен на localhost. Поскольку на ваших машинах разработки у нас, вероятно, нет почтовых серверов, мы добавим новый, указывающий на gmail:
| 1 2 3 4 5 6 7 8 | <subsystemxmlns="urn:jboss:domain:mail:2.0">    <mail-sessionname="default"jndi-name="java:jboss/mail/Default">        <smtp-serveroutbound-socket-binding-ref="mail-smtp"/>    </mail-session>    <mail-sessionname="gmail"jndi-name="java:jboss/mail/Gmail"from="your.account@gmail.com">        <smtp-serveroutbound-socket-binding-ref="mail-gmail"ssl="true"username="your.account@gmail.com"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-groupname="standard-sockets"default-interface="public"port-offset="${jboss.socket.binding.port-offset:0}">    <!-- a bunch of stuff here -->    <outbound-socket-bindingname="mail-smtp">        <remote-destinationhost="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-groupname="standard-sockets"default-interface="public"port-offset="${jboss.socket.binding.port-offset:0}">    <!-- a bunch of stuff here -->    <outbound-socket-bindingname="mail-smtp">        <remote-destinationhost="localhost"port="25"/>    </outbound-socket-binding>    <!-- "mail-gmail" is the same name we used in the mail-session config -->    <outbound-socket-bindingname="mail-gmail">        <remote-destinationhost="smtp.gmail.com"port="465"/>    </outbound-socket-binding>        </socket-binding-group> | 
Это оно. Пожалуйста, оставьте комментарий, если у вас есть какие-либо вопросы :). Позже!
| Ссылка: | CDI & EJB: отправка асинхронной почты об успешной транзакции от нашего партнера JCG Родриго Учоа из Code to live. Живи к коду. блог. | 

