Search code examples
databasespringjmsjboss-eap-6xa

Steps to setup distributed transaction management in JBoss


I am trying to implement a distributed transaction (XA) in JBoss EAP 6.2 application server so that multiple datastores can be accessed in a single coordinated atomic transaction. To be more precise, I'd like my transactional service method to write to a database table and to a message queue in such a way that these two operations are either both committed or both rolled back consistently (all or nothing).

My approach is based on the following:

  • Use Spring JTA Transaction Manager
  • Configure the entity manager to use an XA JDBC datasource, defined in JBoss application server and accessed via a JNDI name
  • Use ActiveMQ XA Connection Factory for messaging

The problem I am facing is that only the database operation is rolled back. The message written to ActiveMQ queue is always commited regardless of the transaction being rolled back or not.

Key elements of my configuration:

<tx:jta-transaction-manager/>

<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="jtaDataSource" ref="xaDataSource" />
    ...
    <property name="jpaProperties">
        <props>
            ...
            <prop key="hibernate.transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</prop>
        </props>
    </property>
</bean>

<jee:jndi-lookup id="xaDataSource" jndi-name="xaDataSource"/>

<bean id="xaConnectionFactory" class="org.apache.activemq.ActiveMQXAConnectionFactory">
    <property name="brokerURL"> 
        <value>tcp://localhost:61616</value> 
    </property> 
</bean>

<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
    <property name="connectionFactory" ref="xaConnectionFactory" />
    <property name="defaultDestinationName" value="TEST_QUEUE" />
    <property name="sessionTransacted" value="true"/>
</bean>

Solution

  • I finally got this working. The key was to configure the JMS connection factory within a JBoss Resource Adapter. Detailed steps below:

    1. Install Apache ActiveMQ

    Version used: 5.11.3

    Detailed install instructions can be found here.

    Once ActiveMQ installed, Create a queue named TEST_QUEUE (use admin console: http://127.0.0.1:8161/admin/index.jsp)

    2. Set up Spring application context

    Key elements:

    • Use Spring JTA Transaction Manager tag: this will prompt the use of the app server transaction manager;
    • Configure the datasource bean to use the XA datasource defined in the app server (see XA JDBC datasource setup);
    • Wire the jtaDataSource attribute of the Entity Manager Factory to the XA datasource;
    • Set Hibernate property manager_lookup_class to JBossTransactionManagerLookup;
    • Configure the connection factory bean to use the XA connection factory bean defined in the app server (see XA Connection Factory setup);
    • Set property transactedSession of connection factory bean to false.

    e

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
        xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
        xmlns:jms="http://www.springframework.org/schema/jms"   xmlns:jee="http://www.springframework.org/schema/jee"
        xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
            http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd
            http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
    
    <jpa:repositories base-package="com.company.app.repository" />
    
    <context:component-scan base-package="com.company.app" />
    
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <tx:jta-transaction-manager/>
    
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="jtaDataSource" ref="xaDataSource" />
        <property name="packagesToScan" value="com.company.app.domain" />
        <property name="persistenceProviderClass" value="org.hibernate.ejb.HibernatePersistence" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="databasePlatform" value="org.hibernate.dialect.Oracle10gDialect" />
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</prop>
            </props>
        </property>
    </bean>
    
    <jee:jndi-lookup id="xaDataSource" jndi-name="jdbc/xaDataSource"/>
    
    <bean id="xaConnectionFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="activemq/ConnectionFactory" />
    </bean>
    
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="xaConnectionFactory" />
        <property name="defaultDestinationName" value="TEST_QUEUE" />
        <property name="sessionTransacted" value="false"/>
    </bean>
    

    3. Set up application server (JBoss)

    For a distributed transaction to work, all the datasources involved must be of type XA. JBoss supports JDBC XA datasource (xa-datasource tag) out of the box. The XA configuration for JMS datasource is achieved by defining appropriate Resource Adapter.

    3.1. XA JDBC datasource

    In standalone.xml under <subsystem xmlns="urn:jboss:domain:datasources:1.1"> <datasources> add an XA JDBC datasource:

    <xa-datasource jndi-name="java:/jdbc/xaDataSource" pool-name="jdbc/xaDataSource" enabled="true">
        <xa-datasource-property name="URL">
            jdbc:oracle:thin:@<hostname>:<port_number>/<SID>
        </xa-datasource-property>
        <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource</xa-datasource-class>
        <driver>ojdbc6-11.2.0.3.jar</driver>
        <security>
            <user-name>db_user</user-name>
            <password>password</password>
        </security>
    </xa-datasource>
    

    3.2. XA Conncection Factory

    Resource adapters are a concept from the J2EE Connector Architecture (JCA) and are used to interface with Enterprise Information Systems, i.e. systems external to the application server (e.g., relational databases, mainframes, Message-Oriented Middleware, accounting systems, etc.).

    First you need to install ActiveMQ RAR (Resource adapter ARchive) in JBoss, by dropping the appropriate RAR file from maven central under \standalone\deployments. Then, in standalone.xml, under <subsystem xmlns="urn:jboss:domain:resource-adapters:1.1"> add the following:

    <resource-adapters>
        <resource-adapter id="activemq-rar.rar">
            <archive>
                activemq-rar-5.11.3.rar
            </archive>
            <transaction-support>XATransaction</transaction-support>
            <config-property name="Password">
                admin
            </config-property>
            <config-property name="UserName">
                admin
            </config-property>
            <config-property name="ServerUrl">
                tcp://localhost:61616?jms.rmIdFromConnectionId=true
            </config-property>
            <connection-definitions>
                <connection-definition class-name="org.apache.activemq.ra.ActiveMQManagedConnectionFactory" jndi-name="java:/activemq/ConnectionFactory" enabled="true" pool-name="ConnectionFactory">
                    <xa-pool>
                        <min-pool-size>1</min-pool-size>
                        <max-pool-size>20</max-pool-size>
                        <prefill>false</prefill>
                        <is-same-rm-override>false</is-same-rm-override>
                    </xa-pool>
                </connection-definition>
            </connection-definitions>
        </resource-adapter>
    </resource-adapters>
    

    For further details about installing ActiveMQ RAR in JBoss, refer to RedHat documentation.

    4. Make your Service method Transactional

    @Service
    public class TwoPhaseCommitService {
    
        @Autowired
        private EmployeeRepository employeeRepository;
    
        @Autowired
        private JmsTemplate jmsTemplate;
    
        @Transactional
        public void writeToDbAndQueue() {
    
            final Employee employee = new Employee();
    
            employee.setFirstName("John");
            employee.setLastName("Smith");
    
            // persist entity to database
            employeeRepository.save(employee);
    
            // write message to TEST_QUEUE
            jmsTemplate.send(new MessageCreator() {
                public Message createMessage(Session session) throws JMSException {
                    return session.createTextMessage(employee.getFirstName());
                }
            });
    
            // To test rollback uncomment code below:
            // throw new RuntimeException("something went wrong. Transaction must be rolled back!!!");
        }
    }