Tomcat 6 Integration with Atomikos 3.5.2+ with lifecycle support
It is possible to fully integrate the Atomikos transaction manager into Tomcat. Doing it this way makes the transaction manager shared across all web applications exactly like with any full-blown J2EE server. Additional efforts have to be made if the resource are configured using JNDI. Tomcat's JNDI does not include a lifecycle management. Therefore it has to be implemented.
A feature like this is needed to be able to start and stop a webapp without running into duplicate resource exceptions.
This implementation is also webapp aware. This is useful to access the same JNDI resource from more than one webapp.
- Important note
- Installation
- Create server lifecycle listener
- Create context lifecycle listener
- Create lifecycle manager
- Create JNDI factory for Atomikos
- Compile and copying the extensions
- Copying TransactionsEssentials libraries
- Copying Atomikos configuration file
- Edit server.xml
- Edit global context.xml
- Edit webapp context.xml
- Using WebSphere MQ
Important note
When the Atomikos transaction manager is installed globally in Tomcat, you now must also install your JDBC driver at the same global location (ie: into the TOMCAT_HOME/lib
folder). If you dont do that, you will get a NoClassDefFoundErrors or a ClassNotFoundException or even a ClassCastException during your web application deployment.
This is not a limitation of Atomikos nor of Tomcat but of the J2EE class loading design that both Tomcat and Atomikos must follow.
Installation
Installation is quite simple, it just involves compling source code into JAR files and copying them, create a property file and editing some Tomcat configuration files.
Create server lifecycle listener
The LifecycleListener has to be changed since release 3.5.2. The first class which callsUserTransactionManager.init() is the master for UserTransactionManager. It is not the first class which calls newUserTransactionManager(). Only the master closes UserTransactionManager with its close() method. ThereforeUserTransactionManager.init() has to be called after the new operator. The master UserTransactionManager has be created by a lifecycle listener to have the manager available independent of the availability of webapps.
AtomikosLifecycleListener.java
package com.atomikos.tomcat; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import com.atomikos.icatch.jta.UserTransactionManager; import com.atomikos.icatch.system.Configuration; public class AtomikosLifecycleListener implements LifecycleListener { private UserTransactionManager utm; public void lifecycleEvent(LifecycleEvent event) { try { if (Lifecycle.START_EVENT.equals(event.getType())) { if (utm == null) { utm = new UserTransactionManager(); } utm.init(); } else if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) { if (utm != null) { utm.close(); } } } catch (Exception e) { System.err.println(e.getMessage()); } } }
Create context lifecycle listener
Here starts the the lifecycle management. This listener informs the lifecycle manager when a webapp starts or stops.
ContextLifecycleListener.java
package com.atomikos.tomcat; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; public class ContextLifecycleListener implements LifecycleListener { private String webappName = null; public void setWebappName(String name) { webappName = name; } public void lifecycleEvent(LifecycleEvent event) { try { if (Lifecycle.START_EVENT.equals(event.getType())) { AtomikosLifecycleManager.getInstance().startWebApp(webappName); } else if (Lifecycle.STOP_EVENT.equals(event.getType())) { AtomikosLifecycleManager.getInstance().stopWebApp(webappName); } } catch (Exception e) { System.err.println(e.getMessage()); } } }
Create lifecycle manager
This manager maintains a list of Atomikos resources for every webapp. Atomikos resources of a named webapp are closed on request from context lifecycle listener.
AtomikosLifecycleManager.java
package com.atomikos.tomcat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.atomikos.jdbc.AtomikosDataSourceBean; import com.atomikos.jms.AtomikosConnectionFactoryBean; public class AtomikosLifecycleManager { private static AtomikosLifecycleManager manager = null; private String webappName = null; final private ConcurrentMap<String, List<Object>> atomikosObjectMap = new ConcurrentHashMap<String, List<Object>>(); private AtomikosLifecycleManager() { } public synchronized static AtomikosLifecycleManager getInstance() { if (manager == null) { manager = new AtomikosLifecycleManager(); } return (manager); } public void startWebApp(String name) { webappName = name; atomikosObjectMap.put(name, new ArrayList<Object>()); } public String getWebappName() { return (webappName); } public void addResource(Object obj) { if (atomikosObjectMap.containsKey(webappName)) { atomikosObjectMap.get(webappName).add(obj); } } public void stopWebApp(String name) { if (atomikosObjectMap.containsKey(name)) { List<Object> list = atomikosObjectMap.get(name); for (Object obj : list) { if (obj instanceof AtomikosConnectionFactoryBean) { ((AtomikosConnectionFactoryBean) obj).close(); } else if (obj instanceof AtomikosDataSourceBean) { ((AtomikosDataSourceBean) obj).close(); } } list.clear(); } } }
Create JNDI factory for Atomikos
JNDI factory to create connection and data source beans. The beans are initialized after creation.
AtomikosTomcatFactoryFactory.java
package com.atomikos.tomcat; import java.util.Enumeration; import java.util.Hashtable; import javax.jms.JMSException; import javax.naming.Context; import javax.naming.Name; import javax.naming.NamingException; import javax.naming.RefAddr; import javax.naming.Reference; import javax.naming.spi.ObjectFactory; import org.apache.naming.ResourceRef; import com.atomikos.beans.PropertyException; import com.atomikos.beans.PropertyUtils; import com.atomikos.jdbc.AtomikosDataSourceBean; import com.atomikos.jdbc.AtomikosSQLException; import com.atomikos.jdbc.nonxa.AtomikosNonXADataSourceBean; import com.atomikos.jms.AtomikosConnectionFactoryBean; public class AtomikosTomcatFactoryFactory implements ObjectFactory { public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { if (obj instanceof ResourceRef) { try { Reference ref = (Reference) obj; String beanClassName = ref.getClassName(); Class<?> beanClass = null; ClassLoader tcl = Thread.currentThread().getContextClassLoader(); if (tcl != null) { try { beanClass = tcl.loadClass(beanClassName); } catch (ClassNotFoundException e) { throw new NamingException("Could not load class " + beanClassName); } } else { try { beanClass = Class.forName(beanClassName); } catch (ClassNotFoundException e) { throw new NamingException("Could not load class " + beanClassName); } } if (beanClass == null) { throw new NamingException("Class not found: " + beanClassName); } if (AtomikosDataSourceBean.class.isAssignableFrom(beanClass)) { return createDataSourceBean(ref, (AtomikosDataSourceBean) beanClass.newInstance()); } else if (AtomikosNonXADataSourceBean.class.isAssignableFrom(beanClass)) { return createNonXADataSourceBean(ref, (AtomikosNonXADataSourceBean) beanClass.newInstance()); } else if (AtomikosConnectionFactoryBean.class.isAssignableFrom(beanClass)) { return createConnectionFactoryBean(ref, (AtomikosConnectionFactoryBean) beanClass.newInstance()); } else { throw new NamingException( "Class is neither an AtomikosDataSourceBean nor an AtomikosConnectionFactoryBean: " + beanClassName); } } catch (InstantiationException e) { throw (NamingException) new NamingException( "error creating AtomikosDataSourceBean or AtomikosConnectionFactoryBean").initCause(e); } catch (IllegalAccessException e) { throw (NamingException) new NamingException( "error creating AtomikosDataSourceBean or AtomikosConnectionFactoryBean").initCause(e); } catch (JMSException e) { throw (NamingException) new NamingException("error creating AtomikosConnectionFactoryBean").initCause(e); } catch (AtomikosSQLException e) { throw (NamingException) new NamingException("error creating AtomikosDataSourceBean").initCause(e); } catch (PropertyException e) { throw (NamingException) new NamingException( "error creating AtomikosDataSourceBean or AtomikosConnectionFactoryBean").initCause(e); } } return (null); } private Object createConnectionFactoryBean(Reference ref, AtomikosConnectionFactoryBean bean) throws JMSException { Enumeration<RefAddr> en = ref.getAll(); int i = 0; while (en.hasMoreElements()) { RefAddr ra = (RefAddr) en.nextElement(); String propName = ra.getType(); String value = (String) ra.getContent(); /** * uniqueResourceName is only unique per webapp but has to made * unique globally. */ if (propName.equals("uniqueResourceName")) { value = AtomikosLifecycleManager.getInstance().getWebappName() + "/" + value; } try { PropertyUtils.setProperty(bean, propName, value); i++; } catch (PropertyException pe) { System.out.println("Property " + propName + "could not be set. " + pe.getMessage()); } } bean.init(); AtomikosLifecycleManager.getInstance().addResource(bean); return (bean); } private Object createNonXADataSourceBean(Reference ref, AtomikosNonXADataSourceBean bean) throws AtomikosSQLException, PropertyException { if (logger.isDebugEnabled()) { logger.debug("instanciating bean of class " + bean.getClass().getName()); } Enumeration<RefAddr> en = ref.getAll(); int i = 0; while (en.hasMoreElements()) { RefAddr ra = (RefAddr) en.nextElement(); String propName = ra.getType(); String value = (String) ra.getContent(); /** * uniqueResourceName is only unique per webapp but has to made * unique globally. */ if (propName.equals("uniqueResourceName")) { value = AtomikosLifecycleManager.getInstance().getWebappName() + "/" + value; } if (logger.isDebugEnabled()) { logger.debug("setting property '" + propName + "' to '" + value + "'"); } try { PropertyUtils.setProperty(bean, propName, value); i++; } catch (PropertyException pe) { System.out.println("Property " + propName + "could not be set. " + pe.getMessage()); } } bean.init(); AtomikosLifecycleManager.getInstance().addResource(bean); return (bean); } private Object createDataSourceBean(Reference ref, AtomikosDataSourceBean bean) throws AtomikosSQLException { if (logger.isDebugEnabled()) { logger.debug("instanciating bean of class " + bean.getClass().getName()); } Enumeration<RefAddr> en = ref.getAll(); int i = 0; while (en.hasMoreElements()) { RefAddr ra = (RefAddr) en.nextElement(); String propName = ra.getType(); String value = (String) ra.getContent(); /** * uniqueResourceName is only unique per webapp but has to made * unique globally. */ if (propName.equals("uniqueResourceName")) { value = AtomikosLifecycleManager.getInstance().getWebappName() + "/" + value; } if (logger.isDebugEnabled()) { logger.debug("setting property '" + propName + "' to '" + value + "'"); } try { PropertyUtils.setProperty(bean, propName, value); i++; } catch (PropertyException pe) { System.out.println("Property " + propName + "could not be set. " + pe.getMessage()); } } bean.init(); AtomikosLifecycleManager.getInstance().addResource(bean); return (bean); } }
Compile and copying the extensions
- Compile the source code into a library
- Copy the library to
TOMCAT_HOME/lib
Copying TransactionsEssentials libraries
- Drop the following JARs from the Atomikos distribution to
TOMCAT_HOME/lib
folder:- transactions.jar
- transactions-api.jar
- transactions-jta.jar
- transactions-jdbc.jar
- atomikos-util.jar.jar
- jta.jar
You should also copy the transactions-hibernate3.jar and/or transactions-hibernate2.jar at the same location if you're planning to use Hibernate.
Copying Atomikos configuration file
- Drop the following properties file into the
TOMCAT_HOME/lib
folder: jta.properties
Edit server.xml
Then edit the TOMCAT_HOME/conf/server.xml
file. At the beginning of the file you should see these four lines:
<Listener className="org.apache.catalina.core.AprLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" /> <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> <Listener className="org.apache.catalina.storeconfig.StoreConfigLifecycleListener"/>
Right after the last one, add this fifth one:
<Listener className="com.atomikos.tomcat.AtomikosLifecycleListener" />
Edit global 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" />
Edit webapp context.xml
Then edit META-INF/context.xml
from a war file. Add at the beginning of section Context.
<?xml version="1.0" encoding="UTF-8"?> <Context> <Listener className="com.atomikos.tomcat.ContextLifecycleListener" webappName="test" /> ... </Context>
Using MySQL
Notice: testQuery is very important for connection management.
<?xml version="1.0" encoding="UTF-8"?> <Context reloadable="true" crossContext="true"> <Resource name="jdbc/myDB" auth="Container" type="com.atomikos.jdbc.AtomikosDataSourceBean" factory="com.atomikos.tomcat.AtomikosTomcatFactoryFactory" uniqueResourceName="jdbc/myDB" xaDataSourceClassName="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource" xaProperties.databaseName="test" xaProperties.serverName="localhost" xaProperties.port="3306" xaProperties.user="USER" xaProperties.password="PASSWORD" xaProperties.url="jdbc:mysql://localhost:3306/test" testQuery="select 1" /> </Context>
Remember to change the parameter values to your specific environment...
Using WebSphere MQ
MQ client must operate in bind mode to be able use MQ in global transactions.
<?xml version="1.0" encoding="UTF-8"?> <Context reloadable="true" crossContext="true"> <Resource name="jms/myQCF" auth="Container" type="com.atomikos.jms.AtomikosConnectionFactoryBean" factory="com.atomikos.tomcat.AtomikosTomcatFactoryFactory" uniqueResourceName="QCF_MQSeries_XA" xaConnectionFactoryClassName="com.ibm.mq.jms.MQXAQueueConnectionFactory" xaProperties.queueManager="XXX" maxPoolSize="3" minPoolSize="1" /> <Resource name="jms/myQ" auth="Container" type="com.ibm.mq.jms.MQQueue" factory="com.ibm.mq.jms.MQQueueFactory" description="JMS Queue for reading messages" QU="MYQ.IN" /> </Context>