CDI & EJB: Sending asynchronous mail on transaction success

Datetime:2016-08-23 01:59:13          Topic: EJB           Share

Hello again! :)

This time I’ve chosen a common task that most of the time, in my opinion, is done the wrong way: sending e-mails. Not that people can’t figure out how e-mail APIs work, such as JavaMail or Apache’s commons-email . What I usually see as a problem is that they underestimate the need to make the sending mail routine asynchronous, and that it also should only run when the underlying transaction commits successfully (most of the time).

Think of the common use case where a user is shopping online. When he’s done, he will probably want to receive an order confirmation e-mail. The process of making an order is kinda complex: We would usually insert records in a lot of different tables, also possibly delete records to remove items from stock and so forth. All of this, of course, has to be accomplished in a single atomic transaction:

//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
}

Much like the pseudocode above, we usually strive to keep transaction logic out of our code. That is, we use CMT (container managed transactions) to have the container do everything for us and keep our code cleaner. So RIGHT AFTER our method call completes, the EJB container commits our transaction. This is problem number 1: When the sendOrderConfirmationMail() method gets called, we have no way of knowing if the transaction will succeed. The user might receive a confirmation for an order that doesn’t exist.

If this is something you have no yet realized, just run a test in any of your codes. Those calls to entityManager.persist() don’t trigger any database commands until our enclosing method call is over. Just put a breakpoint and see for yourself. I’ve seen confusions like these many times.

So in case of a rollback, we do not need to send any emails. Things can go wrong for a number of reasons: system failure, some business rule could deny the purchase, credit card validation, etc.

So we already know that when using CMT we can have a hard time knowing when the transaction is successful or not. The next problem is making the mailing routine asynchronous, completely independent of our order routine. Picture this, what if everything goes fine with the ordering process but some exception occurs when trying to send the email? Should we rollback everything just because our confirmation mail could not be sent? Should we really prevent the user from buying at our store, just because our mail server is having a bad day?

I know that business requirements like this can go either way, but also keep in mind that it’s usually desirable to make the inherent latency of sending mails not interfere with the order processing. Most of the time, processing the order is our main goal. Low priority tasks like sending emails can even be postponed to times when the server load is low.

Here We Go

To tackle this problem I’ve chosen a pure Java EE approach. No third-party APIs need be used. Our environment comprises:

  • JDK 7 or above.
  • Java EE 7 (JBoss Wildfly 8.1.0)
  • CDI 1.1
  • EJB 3.2
  • JavaMail 1.5

I’ve set up a small web project so you can see everything working, download it here if you want .

Before diving into code, just a brief observation: The solution shown below consists mainly on CDI events mixed with EJB async calls. This is because the CDI 1.1 spec doesn’t provide async event processing. It seems that is something being discussed for the CDI 2.0 spec, still on the works. For this reason, a pure CDI approach might be tricky. I’m not saying it’s impossible, I just haven’t even tried.

The code example is just a make believe for a “Register Customer” use case. Where we would send an email to confirm user registration. The overall architecture looks something like this:

The code sample also presents a “fail test case”, so you can actually see that when there’s a rollback no email is sent. I’m only showing you here the “happy path”, starting with the Managed Bean invoking our CustomerService EJB. Nothing interesting, just boilerplate:

Inside our CustomerService EJB things start to get interesting. By using the CDI API we fire an MailEvent event right at the end of the saveSuccess() method:

@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.setTo("some.email@foo.com");
		event.setSubject("Async email testing");
		event.setMessage("Testing email");
		eventProducer.fire(event); //firing event!
	}
}

The MailEvent class is just a regular POJO that represents our event. It encapsulates information about the email message: the recipient, subject, text message, etc:

public class MailEvent {
    private String to; //recipient address
    private String message;
    private String subject;

    //getters and setters
}

If you’re new to CDI and still a bit confused about this event stuff, just read the docs . It should give you an idea.

Next it’s time for the event observer, the MailService EJB. It’s a simple EJB with some JavaMail magic and a couple of annotations you should pay attention to:

@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);
		}
   }
}

Like I said this is just a regular EJB. What makes this class an event observer, more precisely the sendMail() method, is the @Observes annotation in line 9. This annotation alone would make this method run after the event is fired.

But, we need this event to be fired only when the transaction is commited !. A rollback should not trigger email. That’s where the “during” attribute comes in. By specifying the value TransactionPhase.AFTER_SUCCESS we make sure the event is triggered only if the transaction commits successfully.

Last but not least, we also need to make this logic run in a separate thread from our main logic. It has to run asynchronously. And to achieve this we simply used two EJB annotations, @Asynchronous and @Lock(LockType.READ) . The latter, @Lock(LockType.READ) is not required but highly recommended. It guarantees that no locks are used and multiple threads can use the method at the same time.

Configuring the mail session in JBoss Wildfly 8.1.0

As a bonus I’m gonna show how we can correctly configure a mail “source” in JBoss WildFly. Mail sources are pretty much like datasources, except that they’re for sending email, not for database stuff :). It’s a way to keep the code decoupled from how the connection to the mail server is made. I used a connection to my Gmail account, but you could switch to anything you want without having to touch any of the code inside the MailService class.

The javax.mail.Session object can be retrieved by its JNDI name using the @Resource annotation:

@Resource(mappedName = "java:jboss/mail/Gmail")
private Session mailSession;

You probably noticed that in my previous code snippets I did not use the @Resource annotation, I used just CDI’s @Inject . Well, if you’re curious how I did that just download the source code and take a look. ( hint: I used a producer helper class .)

Moving on, just open up the standalone.xml (or domain.xml if you’re in domain mode) and first look for the “mail subsystem”. It should look like this:

<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>

There’s a mail session already provided by default running on localhost. Since we probably do not have any mail servers running in your development machines, we’re gonna add a new one pointing to 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>

See how lines 5, 6 and 7 are highlighted. That’s our new mail session. But that’s not all. We still need to create a socket binding to our new mail session. So inside standalone.xml look for an element called 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>

Now we add our gmail port to the existing ones, by creating a new outbound-socket-binding element:

<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>

This is it. Please leave a comment if you have any questions :). Later!





About List