Tomcat Spring ActiveMQ MySQL JMX Integration
It is possible to fully integrate the Atomikos transaction manager into Tomcat. Also a JMS Provider is added to Tomcat by starting an embedded ActiveMQ server. This document describes an implementation using Spring. This includes bean lifecycle management using Spring. Therefore all resources have to be configured using Spring and not using JNDI of Tomcat. JMX resources of Atomikos are also activated using Spring. Doing it this way makes the transaction manager, JMS Provider and JMX shared across all web applications. MySQL runs as external database server. Therefore only the MySQL JDBC driver has to be added to Tomcat.
Atomikos 3.6.5
Tomcat 6.0.29
Spring Framework 2.5.6
Spring Integration 1.0.4
ActiveMQ 5.4.2
- Installation
- AtomikosLifecycleListener
- atomikos.xml (Spring)
- ActiveMQLifecycleListener
- activemq.xml (Spring)
- Copying the Lifecycle Listener
- Copying TransactionsEssentials libraries
- Copying Atomikos configuration file
- Copying ActiveMQ
- Copying MySQL driver
- Edit server.xml
- Edit context.xml
- Create jdbcconfiguration.properties
- Example JDBC application
- web.xml
- database.xml
- Class InboundSource:
- Example Middleware application
- web.xml
- middleware.xml
Installation
Installation is quite simple, it just involves copying some JAR files, a property file and editing some Tomcat configuration files. The Lifecycle Listeners are implemented using Spring. Both listeners have nearly the same source code, but the different Spring configuration files have to be named somewhere. This configuration starts Atomikos and an embedded ActiveMQ server. MySQL has to be installed and started separately.
AtomikosLifecycleListener
This lifecycle manager starts the Atomikos transaction manager before any web application is started and stops the transaction manager after all web applications stopped.package com.atomikos.tomcat.spring; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AtomikosLifecycleListener implements LifecycleListener { protected ClassPathXmlApplicationContext ctx = null; @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.INIT_EVENT)) { if (ctx == null) { ctx = new ClassPathXmlApplicationContext( "classpath*:com/atomikos/tomcat/spring/atomikos.xml"); } } else if (event.getType().equals(Lifecycle.AFTER_STOP_EVENT)) { if (ctx != null) { ctx.close(); } } } }
atomikos.xml (Spring)
This configuration includes JMX support for Atomikos.<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:mbean-export default-domain="atomikos"/> <bean id="AtomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close"> <property name="forceShutdown" value="false"/> </bean> <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"> <property name="locateExistingServerIfPossible" value="true"/> </bean> <bean id="jmxTransactionService" class="com.atomikos.icatch.admin.jmx.JmxTransactionService"> <!-- Optional: show only heuristic problem cases --> <property name="heuristicsOnly" value="true"/> </bean> <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> <property name="beans"> <map> <entry key="atomikos:name=tx-service"> <ref bean="jmxTransactionService"/> </entry> </map> </property> <property name="server" ref="mbeanServer"/> </bean> </beans>
ActiveMQLifecycleListener
This lifecycle manager starts the ActiveMQ broker before any web application is started and stops the broker after all web applications stopped.package com.atomikos.tomcat.spring; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.springframework.context.support.ClassPathXmlApplicationContext; public class !ActiveMQLifecycleListener implements LifecycleListener { protected ClassPathXmlApplicationContext ctx = null; @Override public void lifecycleEvent(LifecycleEvent event) { if (event.getType().equals(Lifecycle.INIT_EVENT)) { if (ctx == null) { ctx = new ClassPathXmlApplicationContext( "classpath*:com/atomikos/tomcat/spring/activemq.xml"); } } else if (event.getType().equals(Lifecycle.AFTER_STOP_EVENT)) { if (ctx != null) { ctx.close(); } } } }
activemq.xml (Spring)
Spring configuration file to start and stop the ActiveMQ broker. The start and stop actions are forwarded behind the scenes to the BrokerFactoryBean using the Spring interfaces InitializingBean and DisposableBean. The configuration of the broker is defined by ${catalina.base}/conf/activemq.xml.<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="broker" class="org.apache.activemq.xbean.BrokerFactoryBean"> <property name="config" value="file:${catalina.base}/conf/activemq.xml"/> <property name="start" value="true"/> </bean> </beans>
Copying the Lifecycle Listener
Create a jar including the lifecycle listener classes and Spring configuration files. Copy the jar into the TOMCAT_HOME/lib folder.
Copying TransactionsEssentials libraries
Drop the following JARs from the Atomikos distribution into the TOMCAT_HOME/lib folder: transactions.jar transactions-api.jar transactions-jta.jar transactions-jdbc.jar atomikos-util.jar.jar jta.jar
Copying Atomikos configuration file
Drop the following properties file into the TOMCAT_HOME/lib folder: jta.properties
Copying ActiveMQ
Copy ActiveMQ libraries into the TOMCAT_HOME/lib folder: activemq-core-5.4.2.jar activemq-pool-5.4.2.jar activemq-web-5.4.2.jar (for activemq-admin)Create and copy an ActiveMQ configuration file (activemq.xml) into the TOMCAT_HOME/conf folder.
Copying MySQL driver
Copy MySQL driver into the TOMCAT_HOME/lib folder: mysql-connector-java-5.1.13-bin.jar
Edit server.xml
Then edit the TOMCAT_HOME/conf/server.xml file. At the beginning of the file you should see these four lines:<!-- APR library loader. Documentation at /docs/apr.html --> <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> <!-- Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html --> <Listener className="org.apache.catalina.core.JasperListener" /> <!-- Prevent memory leaks due to use of particular java/javax APIs--> <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> <!-- JMX Support for the Tomcat server. Documentation at /docs/non-existent.html --> <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> Right after the last one, add these lines: <Listener className="com.atomikos.tomcat.spring.AtomikosLifecycleListener"/> <Listener className="com.atomikos.tomcat.spring.!ActiveMQLifecycleListener"/>
Edit context.xml
Then edit the TOMCAT_HOME/conf/context.xml file. At the beginning of the file you should see this line:<WatchedResource>WEB-INF/web.xml</WatchedResource>Right after it, add that one:
<Transaction factory="com.atomikos.icatch.jta.UserTransactionFactory"/>
Create jdbcconfiguration.properties
Then create the TOMCAT_HOME/lib/jdbcconfiguration file. This file is used to define a JDBC resource without setting connection properties in a web application.
user=username password=password url=jdbc:mysql://localhost:3306/example?pinGlobalTxToPhysicalConnection=true(Change properties according to your environment)
Example JDBC application
Here is a sample application snipplet that demonstrates how you can run TransactionsEssentials in a web application after it has been globally installed. The global installation already configured TransactionsEssentials. Therefore the J2ee version of manager and transactions can be used.
web.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'> <web-app> <display-name>Database Spring Flow</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/database.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
database.xml
Spring flow snipplet from database.xml using MySQL and Spring Integration:<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:spi="http://www.springframework.org/schema/integration" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-1.0.xsd"> <spi:channel id="inputChannel"/> <bean id="AtomikosTransactionManager" class="com.atomikos.icatch.jta.J2eeTransactionManager"/> <bean id="AtomikosUserTransaction" class="com.atomikos.icatch.jta.J2eeUserTransaction"/> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager" ref="AtomikosTransactionManager"/> <property name="userTransaction" ref="AtomikosUserTransaction"/> </bean> <bean id="inboundSource" class="com.atomikos.tomcat.spring.InboundSource" init-method="init"> <property name="dataSource" ref="xaDataSource"/> <property name="sqlString" value="SELECT 1"/> </bean> <spi:inbound-channel-adapter id="inboundAdapter" ref="inboundSource" method="receive" channel="inputChannel"> <spi:poller> <spi:cron-trigger expression="0/5 * * * * 1-5"/> <spi:transactional transaction-manager="transactionManager" propagation="REQUIRED"/> </spi:poller> </spi:inbound-channel-adapter> <!-- .......... --> <util:properties id="jdbcConfiguration" location="classpath:jdbcconfiguration.properties"/> <bean id="xaDataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy-method="close"> <property name="uniqueResourceName" value="DataSourceTransaction"/> <property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/> <property name="minPoolSize" value="2"/> <property name="maxPoolSize" value="5"/> <property name="xaProperties" ref="jdbcConfiguration"/> <property name="testQuery" value="select 1"/> </bean> </beans>
Class InboundSource:
This class is not a real world one. It shows very briefly how to forward JDBC result sets to a Spring Integration channel. A real world class has to be implemented with a ResultSetExtractor to convert a ResultSet from an real world sql statement into a proper Message.
package com.atomikos.tomcat.spring; import javax.sql.DataSource; import org.springframework.integration.core.Message; import org.springframework.integration.message.MessageBuilder; import org.springframework.integration.message.MessageSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.util.Assert; public class InboundSource implements MessageSource<Integer> { protected JdbcTemplate jdbcTemplate = null; protected String statement = null; public void setDataSource(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } public void setSqlString(String sqlString) { this.statement = sqlString; logger.debug(sqlString); } @Override public Message<Integer> receive() { String sqlString = new String(statement); int result = jdbcTemplate.queryForInt(sqlString); if (document != null) { return (MessageBuilder.withPayload(new Integer(result)).build()); } return (null); } public void init() throws Exception { Assert.notNull(jdbcTemplate, "Data Source object must not be null"); Assert.notNull(statement, "SQL String must not be null"); } }
Example Middleware application
Here is a sample application snipplet that demonstrates how you can run TransactionsEssentials in a web application after it has been globally installed. The global installation already configured TransactionsEssentials. Therefore the J2ee version of manager and transactions can be used. It is a simple blueprint application that shows accessing ActiveMQ.
web.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'> <web-app> <display-name>Database Spring Flow</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/middleware.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> </web-app>
middleware.xml
Spring flow snipplet using ActiveMQ and Spring Integration to read from a queue. The property sessionTransacted has to be set to "true". This differs to a "real" J2EE server because a J2EE server sets session transacted to true by default. Spring flow snipplet from middleware.xml using ActiveMQ and Spring Integration:<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:spi="http://www.springframework.org/schema/integration" xmlns:amq="http://activemq.apache.org/schema/core" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-1.0.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core-5.3.0.xsd"> <spi:channel id="inputChannel"/> <bean id="AtomikosTransactionManager" class="com.atomikos.icatch.jta.J2eeTransactionManager"/> <bean id="AtomikosUserTransaction" class="com.atomikos.icatch.jta.J2eeUserTransaction"/> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManager" ref="AtomikosTransactionManager"/> <property name="userTransaction" ref="AtomikosUserTransaction"/> </bean> <bean id="ConnectionFactory" class="com.atomikos.jms.AtomikosConnectionFactoryBean" init-method="init" destroy-method="close"> <property name="uniqueResourceName" value="ExampleTransaction"/> <property name="xaConnectionFactory" ref="XAQCF"/> </bean> <bean id="ListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="receiveTimeout" value="10000"/> <property name="connectionFactory" ref="ConnectionFactory"/> <property name="destination" ref="QueueDestination"/> <property name="transactionManager" ref="transactionManager"/> <property name="sessionTransacted" value="true"/> <property name="sessionAcknowledgeMode" value="0"/> </bean> <bean id="JmsInput" class="org.springframework.integration.jms.JmsMessageDrivenEndpoint"> <constructor-arg ref="ListenerContainer"/> <constructor-arg ref="MessageListener"/> </bean> <bean id="MessageListener" class="com.westlb.traderouter.jms.listener.ChannelPublishingJmsMessageListener"> <property name="requestChannel" ref="inputChannel"/> <property name="expectReply" value="false"/> </bean> <!-- .......... --> <amq:queue id="QueueDestination" physicalName="EXAMPLE.QUEUE"/> <amq:xaConnectionFactory id="XAQCF" brokerURL="vm://localhost?create=false"/> <beans>