We had a scenario where we are not using an application server like JBoss (WildFly), WebSphere, Welogic etc. We create web application archive and deploy them in a basic servlet container like Tomcat, Jetty etc. In our case we use Tomcat 7.x. We also needed both XA as well non-XA datasource requirements since we have business cases that supports single or multiple resources. As I was browsing through the internet I did not find a decent article that showed the configurations clearly and we ran into a lot of problems until we got it finally working. In this article, I will try to articulate the configuration settings with Spring based annotations.
Stack:
Tomcat 7.x
Spring 3.2.1
Standalone Narayna/JBoss TS (5.0.1.Final)
Hibernate 4.1.7 (ORM layer)
Apache DBCP 2 (2.0.0)
JMS Pool from ActiveMq project (5.9.0)
HornetQ 2.2.18
PostgresSql 9
List of Maven Dependencies:
I am listing only those dependencies that are transactions related. You will still need other Spring, logging Hibernate dependencies etc.
Common Dependencies for both Database as well JMS transactions. This is typically created in its own platform-commons.jar that could be used in you DAL and MOM layer for transaction support.
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>3.2.1.RELEASE</version> </dependency> <dependency> <groupId>org.jboss.narayana.jta</groupId> <artifactId>narayana-jta</artifactId> <version>5.0.1.Final</version> </dependency> <!--jboss-transaction-spi is needed cos of a missing transitive dependency in narayana-full.jar. This will be fixed in release 5.0.3. You will not need to include this dependency then --> <dependency> <groupId>org.jboss</groupId> <artifactId>jboss-transaction-spi</artifactId> <version>7.1.0.Final</version> <exclusions> <exclusion> <groupId>*</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.0.0</version> </dependency>
Configuration in Spring to bootstrap PlatformTransactionManager and JBoss TransactionManager in platform-commons.jar
TransactionConfig.java
import javax.transaction.SystemException; import javax.transaction.TransactionManager; import javax.transaction.UserTransaction; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.jta.JtaTransactionManager; import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple; import com.arjuna.ats.internal.jta.transaction.arjunacore.UserTransactionImple; @Configuration @EnableTransactionManagement public class TransactionConfig { @Value("${transaction.timeout:60}") private Integer transactionTimeOut; @Bean public PlatformTransactionManager jtaTransactionManager() { return new JtaTransactionManager(usrTransactionManager(), userTransaction()); } @Bean public TransactionManager userTransaction() { TransactionManagerImple transactionManager = new TransactionManagerImple(); try { transactionManager.setTransactionTimeout(transactionTimeOut); } catch (SystemException e) { throw new RunTimeException("Transaction timed out"); } return transactionManager; } @Bean public UserTransaction usrTransactionManager() { UserTransactionImple utm = new UserTransactionImple(); try { utm.setTransactionTimeout(transactionTimeOut); } catch (SystemException e) { throw new RunTimeException("Transaction timed out"); } return utm; } }
Once you have wired Spring and JBoss TS, now we will set up DAL layer with XA aware and non-xa Datasources and wire up the db connections. This is typically done in platform jar like platform-dal.jar where platform-commons from above is a dependency. In the following class, we are explicitly setting up PostgresSQL datasources that are XA and non-XA aware. All the configuration values (@Value properties) can be injected from a property file which is typical in a Spring application. Also, note that we are using Apache DBCP2 pooling for pooling our connections. Although there are some negative remarks about DBCP on the internet, we found it to work optimally especially the newer version. Other options was BoneCP but you cannot do XA pooling.
PgConfig.java
import javax.inject.Inject; import javax.sql.DataSource; import javax.sql.XADataSource; import javax.transaction.TransactionManager; import org.apache.commons.dbcp2.managed.BasicManagedDataSource; import org.apache.commons.dbcp2.managed.DataSourceXAConnectionFactory; import org.postgresql.xa.PGXADataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration public class PgConfig { // This value not to be provided by applications using this module and will always default to using the provided PostgreSQL JDBC driver. private String driverClass = "org.postgresql.Driver"; @Value("${pg.jdbc.url}") private String jdbcUrl; @Value("${pg.username}") private String userName; @Value("${pg.password}") private String password; @Value("${pg.min.pool.size:1}") private Integer minPoolSize; @Value("${pg.max.pool.size:5}") private Integer maxPoolSize; @Value("${pg.login.time.out:60}") private Integer loginTimeOut; @Value("${pg.database.name}") private String databaseName; @Value("${pg.server.name}") private String serverName; @Value("${pg.server.port}") private Integer portNumber; @Value("${application.name}") private String applicationName; @Inject private TransactionManager transactionManager; @Bean(destroyMethod = "close") public DataSource pgNonXaDataSource() { BasicManagedDataSource dataSource = new BasicManagedDataSource(); dataSource.setDriverClassName(driverClass); dataSource.setUrl(jdbcUrl); dataSource.setUsername(userName); dataSource.setPassword(password); dataSource.setInitialSize(minPoolSize); dataSource.setMaxTotal(maxPoolSize); dataSource.setTransactionManager(transactionManager); return dataSource; } @Bean(name = "pgXAConnectionFactory") public DataSourceXAConnectionFactory xaConnectionFactory() { return new DataSourceXAConnectionFactory(transactionManager, pgXaDataSourceProperties()); } @Bean public DataSource pgXaDataSource() { BasicManagedDataSource dataSource = new BasicManagedDataSource(); dataSource.setDriverClassName(driverClass); dataSource.setUrl(jdbcUrl); dataSource.setUsername(userName); dataSource.setPassword(password); dataSource.setInitialSize(minPoolSize); dataSource.setMaxTotal(maxPoolSize); dataSource.setTransactionManager(transactionManager); dataSource.setXaDataSourceInstance(pgXaDataSourceProperties()); return dataSource; } private XADataSource pgXaDataSourceProperties() { PGXADataSource pgxaDataSource = new PGXADataSource(); pgxaDataSource.setServerName(serverName); pgxaDataSource.setPortNumber(portNumber); pgxaDataSource.setDatabaseName(databaseName); pgxaDataSource.setUser(userName); pgxaDataSource.setPassword(password); pgxaDataSource.setApplicationName(applicationName); return pgxaDataSource; } }
Now, we need to wire up Spring's EntityManagerFactoryBean with the xa and non-xa DataSources we defined earlier:
PgJTAConfig.java
import java.util.Properties; import javax.inject.Inject; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableTransactionManagement public class PgJtaConfig { @Value("${hibernate.show.sql:false}") private Boolean hibernate_show_sql; @Value("${hibernate.format.sql:false}") private Boolean hibernate_format_sql; @Inject private PgConfig pgConfig; @Inject private SpringDalConfig springDalConfig; @Bean public LocalContainerEntityManagerFactoryBean pgJtaXaEntityManagerFactory() { LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); localContainerEntityManagerFactoryBean.setJtaDataSource(pgConfig.pgXaDataSource()); localContainerEntityManagerFactoryBean.setJpaDialect(hibernateJpaDialect()); localContainerEntityManagerFactoryBean.setJpaProperties(setJpaProperties()); localContainerEntityManagerFactoryBean.setPersistenceUnitName("pg-xa"); return localContainerEntityManagerFactoryBean; } @Bean public LocalContainerEntityManagerFactoryBean pgJtaNonXaEntityManagerFactory() { LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); localContainerEntityManagerFactoryBean.setJtaDataSource(pgConfig.pgNonXaDataSource()); localContainerEntityManagerFactoryBean.setJpaDialect(hibernateJpaDialect()); localContainerEntityManagerFactoryBean.setJpaProperties(setJpaProperties()); localContainerEntityManagerFactoryBean.setPersistenceUnitName("pg-non-xa"); return localContainerEntityManagerFactoryBean; } private HibernateJpaDialect hibernateJpaDialect() { return new HibernateJpaDialect(); } private Properties setJpaProperties() { Properties props = new Properties(); props.setProperty("hibernate.transaction.manager_lookup_class", JBossTransactionManagerLookup.class.getName()); props.put("hibernate.show_sql", hibernate_show_sql); props.put("hibernate.format_sql", hibernate_format_sql); return props; } }
We will need one another class that will hookup JBoss TS and Hibernate which is referred in the above setJpaProperties() method, namely JBossTransactionManagerLookup
JBossTransactionManagerLookup.java
import java.util.Properties; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.hibernate.HibernateException; import org.hibernate.transaction.TransactionManagerLookup; import org.springframework.context.annotation.Configuration; @Configuration public class JBossTransactionManagerLookup implements TransactionManagerLookup { private static final TransactionManager TRANSACTION_MANAGER_INSTANCE; static { TRANSACTION_MANAGER_INSTANCE = com.arjuna.ats.jta.TransactionManager.transactionManager(); if (null == TRANSACTION_MANAGER_INSTANCE) { throw new HibernateException("Could not obtain arjuna Transaction Manager instance."); } } public TransactionManager getTransactionManager(Properties props) { return TRANSACTION_MANAGER_INSTANCE; } public String getUserTransactionName() { return null; } public Object getTransactionIdentifier(Transaction transaction) { return transaction; } }
Now, your persistence.xml would look something like this:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="pg-xa" transaction-type="JTA"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="hibernate.archive.autodetection" value="class,hbm"/> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> </properties> </persistence-unit> <persistence-unit name="pg-non-xa" transaction-type="JTA"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="hibernate.archive.autodetection" value="class,hbm"/> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/> </properties> </persistence-unit> </persistence>
Thats all to the DAL layer. Now to the MOM layer, where you want to hook up JBoss Transaction manager with your JMS provider. In our case it is Hornet Q. We used ActiveMq's JMS pooling for our XA pooling of JMS connections. The classes enlisted are typically in a platform-mom.jar that has platform-commons.jar as a dependency from above.
HornetQBase.java
import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; import org.hornetq.api.core.TransportConfiguration; import org.hornetq.core.remoting.impl.netty.NettyConnectorFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @Component public class HornetQBase { @Value("${hornetq.server.url}") private String hornetQServerUrls; @Value("${hornetq.port}") private String hornetQPort; private List<String> transportServers = Lists.newArrayList(); @PostConstruct void initializeTransportServers() { Splitter splitter = Splitter.on(DELEMITER_COMMA).omitEmptyStrings().trimResults(); for (String url : splitter.split(hornetQServerUrls)) { transportServers.add(url); } } TransportConfiguration[] transportConfiguration() { List<TransportConfiguration> transportConfigurations = Lists.newArrayList(); for (String url : transportServers) { Map<String, Object> map = Maps.newHashMap(); map.put(MESSAGING_HOST, url); map.put(MESSAGING_PORT, hornetQPort); TransportConfiguration transportConfiguration = new TransportConfiguration(NettyConnectorFactory.class.getName(), map); transportConfigurations.add(transportConfiguration); } return transportConfigurations.toArray(new TransportConfiguration[transportConfigurations.size()]); } }
JmsHornetQXAConfig.java
import javax.inject.Inject; import javax.jms.ConnectionFactory; import javax.transaction.TransactionManager; import org.apache.activemq.jms.pool.XaPooledConnectionFactory; import org.apache.camel.component.jms.JmsComponent; import org.apache.camel.component.jms.JmsConfiguration; import org.hornetq.api.core.TransportConfiguration; import org.hornetq.jms.client.HornetQXAConnectionFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.jms.core.JmsTemplate; import org.springframework.transaction.PlatformTransactionManager; @Configuration public class JmsHornetQXaConfig { @Inject private HornetQBase hornetQBase; private static final String JMS_XA_RESOURCE_NAME = "jmsXaHornetQ"; @Value("${hornetq.xa.minpoolsize}") private Integer minPoolSize; @Value("${hornetq.xa.maxpoolsize}") private Integer maxPoolSize; @Value("${jms.hornetq.xa.session.transacted:true}") private Boolean isTransacted; @Value("${jms.hornetq.xa.cache.level.name:CACHE_NONE}") private String cacheLevelName; @Value("${jms.hornetq.xa.acknowledgment.mode:transacted}") private String consumerAcknowledgementMode; @Value("${jms.hornetq.session.cache.size:10}") private Integer sessionCacheSize; @Value("${jms.hornetq.cache.producers:true}") private Boolean isCacheProducers; // This value can be overridden by the application connecting to HornetQ. If not provided, it will default to Spring's default of true. @Value("${jms.hornetq.cache.consumers:true}") private Boolean isCacheConsumers; @Value("${jms.hornetq.idletaskexecution.limit:1}") private Integer idleTaskExecutionLimit; @Value("${jms.hornetq.receivetimeout:1000}") private Integer receiveTimeout; @Value("${jms.hornetq.maxmessagepertask:1}") private Integer maxMessagesPerTask; @Value("${jms.hornetq.idleconsumer.limit:1}") private Integer idleConsumerLimit; @Value("${jms.hornetq.reconnect.attempts:-1}") private Integer reconnectAttempts; @Value("${jms.hornetq.retry.interval:10000}") private Long retryInterval; @Value("${jms.hornetq.retry.interval.multiplier:1.5}") private Double retryIntervalMultiplier; @Value("${jms.hornetq.max.retry.interval:60000}") private Long maxRetryInterval; @Value("${hornetq.high.available:false}") private Boolean ha; @Inject private PlatformTransactionManager transactionManager; @Inject private TransactionManager jtaTransactionManager; @Bean public JmsComponent camelJmsHornetQXa() { JmsComponent jmsComponent = new JmsComponent(); jmsComponent.setConfiguration(camelJmsHornetQConfigXa()); return jmsComponent; } private JmsConfiguration camelJmsHornetQConfigXa() { JmsConfiguration jmsConfiguration = new JmsConfiguration(); jmsConfiguration.setConnectionFactory(pooledHornetQConnectionFactory()); jmsConfiguration.setTransacted(isTransacted); jmsConfiguration.setCacheLevelName(cacheLevelName); jmsConfiguration.setTransactionManager(transactionManager); jmsConfiguration.setAcknowledgementModeName(consumerAcknowledgementMode); jmsConfiguration.setIdleTaskExecutionLimit(idleTaskExecutionLimit); jmsConfiguration.setReceiveTimeout(receiveTimeout); jmsConfiguration.setIdleConsumerLimit(idleConsumerLimit); jmsConfiguration.setMaxMessagesPerTask(maxMessagesPerTask); return jmsConfiguration; } @Bean public JmsTemplate hornetQXaJmsTemplate() { JmsTemplate jmsTemplate = new JmsTemplate(pooledHornetQConnectionFactory()); return jmsTemplate; } @Bean(destroyMethod = "stop") public ConnectionFactory pooledHornetQConnectionFactory() { XaPooledConnectionFactory pooledConnectionFactory = new XaPooledConnectionFactory(); pooledConnectionFactory.setMaxConnections(maxPoolSize); pooledConnectionFactory.setConnectionFactory(hornetQXaConnectionFactory()); pooledConnectionFactory.setTransactionManager(jtaTransactionManager); return pooledConnectionFactory; } @Bean public ConnectionFactory hornetQXaConnectionFactory() { TransportConfiguration[] transportConfigurations = hornetQBase.transportConfiguration(); HornetQXAConnectionFactory xaConnectionFactory = new HornetQXAConnectionFactory(ha, transportConfigurations); // Specify number of times the Hornetq client should try to re connect to the server before it destroy the connection xaConnectionFactory.setReconnectAttempts(reconnectAttempts); // Specify long time the next retry will begin xaConnectionFactory.setRetryInterval(retryInterval); // Specify a multiplier to apply to the retryInterval since the last retry to compute the time to the next retry xaConnectionFactory.setRetryIntervalMultiplier(retryIntervalMultiplier); // Specify the max retry interval that can be xaConnectionFactory.setMaxRetryInterval(maxRetryInterval); return xaConnectionFactory; } }
Few things to note above is that we are using PlatformTransactionManager for message consumption using Apache Camel. We using JmsTemplate to send message which is using the pooled connection provided by JMS pool as described above. You will have to use the TransactionManager in Pooled XA connection as it needs to coordinate the 2 phase transaction.
I hope someone will find this article useful while using Standalone JBoss TS in a Spring environment.
Comments