php cdi
再次问好! :)
这次,我选择了一项常见任务,我认为大多数情况下都以错误的方式完成:发送电子邮件。 并非所有人都不知道电子邮件API的工作方式,例如JavaMail或Apache的commons-email 。 我通常看到的一个问题是,它们低估了使发送邮件例程异步的需求,并且该例程也应该仅在基础事务成功提交(大多数情况下)时运行。
想一想用户在线购物的常见用例。 完成后,他可能希望接收订单确认电子邮件。 下订单的过程有点复杂:我们通常会在许多不同的表中插入记录,也可能会删除记录以从库存中删除物品等。 当然,所有这些都必须在单个原子事务中完成:
//A sample EJB method
//(using CMT for transaction management)
public void saveOrder() {//saving some productsentityManager.persist(product1);entityManager.persist(product2);//removing them from stockentityManager.remove(product1);//and at last, we have to send that emailsendOrderConfirmationMail(); //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方法可能会比较棘手。 我并不是说这是不可能的,我什至没有尝试过。
该代码示例仅是一个“注册客户”用例的信条。 我们将在其中发送电子邮件以确认用户注册的位置。 总体架构如下所示:
该代码示例还提供了一个“失败测试用例”,因此您实际上可以看到,在进行回滚时没有发送电子邮件。 我只是在这里向您展示“幸福的道路”,从托管Bean调用我们的CustomerService EJB开始。 没什么有趣的,只是样板:
在我们的CustomerService EJB内部,事情开始变得有趣。 通过使用CDI API,我们可以在saveSuccess()方法的末尾触发MailEvent事件:
@Stateless
public class CustomerService {@Injectprivate EntityManager em;@Injectprivate 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.setTo("some.email@foo.com");event.setSubject("Async email testing");event.setMessage("Testing email");eventProducer.fire(event); //firing event!}
}
MailEvent类只是代表我们事件的常规POJO。 它封装了有关电子邮件的信息:收件人,主题,文本消息等:
public class MailEvent {private String to; //recipient addressprivate String message;private String subject;//getters and setters
}
如果您是CDI的新手,并且对此事件仍然有些困惑, 请阅读docs 。 它应该给您一个想法。
接下来是时候使用事件观察器MailService EJB了。 这是一个简单的EJB,带有一些JavaMail魔术和一些应注意的注释 :
@Singleton
public class MailService {@Injectprivate 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()方法的原因是第9行中的@Observes批注。仅此批注将使此方法在事件触发后运行。
但是,我们只需要在提交事务 !时才触发此事件。 回滚不应触发电子邮件。 这就是“ during”属性的来源。通过指定值TransactionPhase.AFTER_SUCCESS,我们确保仅在事务成功提交后才触发事件。
最后但并非最不重要的一点是,我们还需要使此逻辑与主逻辑在单独的线程中运行。 它必须异步运行。 为此,我们仅使用了两个EJB批注@Asynchronous和@Lock(LockType.READ) 。 后者@Lock(LockType.READ)不是必需的,但强烈建议使用。 它保证不使用锁,并且多个线程可以同时使用该方法。
在JBoss Wildfly 8.1.0中配置邮件会话
作为奖励,我将展示如何在JBoss WildFly中正确配置邮件“源”。 邮件源与数据源非常相似,除了它们用于发送电子邮件而不是用于数据库:)。 这是一种使代码与如何建立与邮件服务器的连接脱钩的方法。 我使用了与我的Gmail帐户的连接,但是您无需切换MailService类中的任何代码即可切换到所需的任何内容。
可以使用@Resource批注以其JNDI名称检索javax.mail.Session对象:
@Resource(mappedName = "java:jboss/mail/Gmail")
private Session mailSession;
您可能已经注意到,在我以前的代码片段中,我没有使用@Resource批注,而仅使用了CDI的@Inject 。 好吧,如果您好奇我是怎么做到的,只需下载源代码并看一下即可。 ( 提示:我使用了生产者帮助器类 。)
继续,只需打开standalone.xml (如果处于域模式,则打开domain.xml),然后首先查找“邮件子系统”。 它看起来应该像这样:
<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>
默认情况下,已经在本地主机上运行了一个已提供的邮件会话。 由于您的开发机器上可能没有运行任何邮件服务器,因此我们将添加一个指向gmail的新邮件服务器:
<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><mail-session name="gmail" jndi-name="java:jboss/mail/Gmail" from="your.account@gmail.com"><smtp-server outbound-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的元素:
<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>
现在,通过创建新的outbound-socket-binding元素,将gmail端口添加到现有端口:
<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>
就是这个。 如果您有任何问题,请发表评论:)。 后来!
翻译自: https://www.javacodegeeks.com/2015/03/cdi-ejb-sending-asynchronous-mail-on-transaction-success.html
php cdi