How can I include a JMS Producer in a Transaction

2019-05-22 21:12发布

问题:

The short question: Is there a way to force a POJO called by a stateless EJB to live in the context of the EJB so that transactions and resource injection will work in the POJO?

Specfically in the context of what I am trying to do: how can I include a POJO JMS Producer in the transaction of an EJB that persists some data in a database before calling the POJO to send the message, such that if the message can't be sent due to an exception, the database transaction will roll back too? I want to send the mail asynchronously.

This is the happy path (starting within stateless session bean):

  • save data to database // this works
  • pull select data from the data that was persisted and place it in a custom 'message' class (really a dto)
  • call the sendEmail method of the EmailQueueMessenger POJO passing it the message object.
  • message is sent to the MDB to process and send the email (not part of the question, just here for completeness)

The code below works, it just won't roll back the database "persist" in the calling class if I force an error in say, a context lookup. BTW, I can't get @Resource injection to work either.

//In the EJB
EmailQueueMessenger eqm = new EmailQueueMessenger();
eqm.sendEmail(messageObject);
// mailObject will be translated into an email message at the other end of the queue.  

/******************** POJO Below ************/  

public class EmailQueueMessenger implements Serializable {

    // Resource injection doesn't work... using 'lookup' below, which does work.
    //    @Resource(name = "jms/EmailerQueueConnectionFactory")
    //    private ConnectionFactory connectionFactory;
    //    @Resource(name = "jms/EmailerQueue")
    //    private Destination EmailerQueue;

        public EmailQueueMessenger() {
        }

        public void sendEmail(MailMessageDTO theMessage) {

            Context ctx = null;
            try {
                ctx = new InitialContext();
                ConnectionFactory connectionFactory = (ConnectionFactory) ctx.lookup("jms/EmailerQueueConnectionFactory");
                System.out.println("JMS Producer CTX Name In Namespace: " + ctx.getNameInNamespace());
                //Destination EmailerQueue = (Destination) ctx.lookup("jms/ERROR"); // forces exception
                Destination EmailerQueue = (Destination) ctx.lookup("jms/EmailerQueue");  // normal working code

                try {
                    Connection con = connectionFactory.createConnection();
                    Session session = con.createSession(false,
                            Session.AUTO_ACKNOWLEDGE);
                    MessageProducer msgProd = session.createProducer(EmailerQueue);

              ...

I have tried adding:

@TransactionAttribute(TransactionAttributeType.MANDATORY)
@Stateless

to the POJO definition but it doesn't make a difference.

FWIW I am using a separate class for the EmailQueueMessenger because there will be other parts of the app that will need to send the occasional email, so don't want to duplicated code.


Should mention that I did a test where I moved all the JMS stuff to within the first EJB and it ran correctly... but I need this to work in a separate class for use by other parts of the app.

回答1:

I think you have 2 issues:

  1. You need to make your pojo an SLSB. It should be injected into your jms listener, not called directly so that you are dealing with the proxy reference. It can still be reused as a simple pojo since the annotations will be ignored if not deployed in a container.

  2. You are creating a jms session using AUTO_ACKNOWLEDGE but it needs to be transacted. Additionally, make sure the jms connection is coming from a transactional JCA source since that will associate the session to the transaction.

========= Update =========

Hey Bill;

Apologies, I thought the outer bean was a JMS listener for some reason..... Anyways, the issue is the same.

If you want EmailQueueMessenger to behave in accordance with the annotations you place on it (transactional, injections etc.) you have to reference it as an EJB, not as a simple pojo. Accordingly, your outer session bean should look like this:

@EJB   // key difference
private EmailQueueMessenger eqm;

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void sendMessage(Object messageObject) {
   eqm.sendEmail(messageObject);
}

Now your

@Resource(name = "jms/EmailerQueueConnectionFactory")
@Resource(name = "jms/EmailerQueue")

and

@TransactionAttribute(TransactionAttributeType.MANDATORY)
@Stateless

annotations will be honored.

Lastly, your JMS sender will be enrolled in a transaction at the point of invocation and you need to make sure that the transaction manager is aware that you are enlisting a second resource manager in the transaction (first the DB , and now JMS). I am not that familiar with glassfish, but it seems there is a configuration screen with a switch that allows you to specify the level of transactional support for a connection factory.

I would change the sender code to:

Session session = con.createSession(true, Session.SESSION_TRANSACTED);

Technically, you can cache the JMS connection instance in the EmailQueueMessenger instance. Your code should not close the JMS session as this will be handled when the transaction completes (although I have seen variances between JMS/JTA implementations on this point).

I hope that clears it up, and I really hope it works !