About

Jadira is the home for Sousan and Chris Pheby's open source projects. These are reusable open source Java modules that provide first class solutions using the most effective current JEE technologies.

Search
Tag Cloud
...
Login
« Batching Transactions for Performance with Spring | Main | Classes for IO »
Saturday
Apr192014

Spring, Hibernate and JTA - A Better Integration

The Problem Space

A perennial issue I have frequently encountered (and solved) lies in the integration of Spring and Hibernate's transaction management.

Both Spring and Hibernate provide distinct transaction manager lookup strategies. In Spring, you'll typically configure a transaction manager by declaring a Spring managed bean that implements the PlatformTransactionManager interface provided by Spring. Included with Spring are a variety of implementations for the most common application servers as well as some non-JTA implementations, that are suitable for testing or limited (single-resource) production uses.

Meanwhile, Hibernate provides its abstraction - the JtaPlatform, the implementing type for which must be configured as part of the Hibernate EntityManager or Session configuration.

The result is that for each deployment environment - unit test, and any planned production container - a separate transaction manager and entity manager must be configured. As an additional problem, if you want to use local transactions in unit tests, the type (JTA or RESOURCE_LOCAL) must be changed in the persistence.xml.

All of this means, that delivery of portable components is fraught with complexity and configuration hell.

A Simplification

A huge simplification could be gained if Hibernate could reference whatever transaction manager you configured in Spring. The Hibernate API requires the capability to access the JTA Transaction Manager, and Spring can provide this - assuming you configured a JtaTransactionManager or one ifs subclasses, which expose both getTransactionManager and getUserTransaction methods.

Assuming we could provide to Hibernate the JtaTransactionManager, we can write an implementation of AbstractJtaPlatform which looks up the resources we need:

    @Override
    protected TransactionManager locateTransactionManager() {
        return transactionManager.getTransactionManager();
    }

    @Override
    protected UserTransaction locateUserTransaction() {
        return transactionManager.getUserTransaction();
    }

Customising LocalContainerEntityManagerFactoryBean

Providing the TransactionManager to this class is tricky. That is because its lifecycle and instantiation is managed by Hibernate, not by Spring. However, assuming you are using Spring to bootstrap the Hibernate Session or EntityManager, the solution is relatively straightforward. I'll discuss the EntityManager case - the Session solution is essentially the same.

It becomes necessary to subclass LocalContainerEntityManagerFactoryBean, and include on the subclass a setter allowing injection of the Spring TransactionManager. Then, the createNativeEntityManagerFactory() method needs overriding:

public class HibernateEntityManagerFactoryBean extends LocalContainerEntityManagerFactoryBean {

    private JtaTransactionManager transactionManager;

    private static final ThreadLocal<JtaTransactionManager> configurationTransactionManagerHolder = new ThreadLocal<JtaTransactionManager>();

    static JtaTransactionManager getConfigurationTransactionManager() {
        return configurationTransactionManagerHolder.get();
    }

    @Override
    protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException {

        if (this.transactionManager != null) {
            configurationTransactionManagerHolder.set(this.transactionManager);
        }

        try {
            return super.createNativeEntityManagerFactory();
        } finally {

            if (this.transactionManager != null) {
                configurationTransactionManagerHolder.set(null);
            }
        }
    }

    /**
     * Associate a transaction manager with this session
     * @param transactionManager The {@link JtaTransactionManager}
     */
    public void setTransactionManager(JtaTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }
}

As Hibernate is bootstrapped, the JtaTransactionManager reference is stored in a static ThreadLocal. We use ThreadLocal to guard against the possibility of Spring being bootstrapped in other threads within the same classloader - at the same time (e.g. within an application server). This ThreadLocal can then be accessed via our JtaPlatform implementation as it initialises. Here's the full code for that:

public class LocalTransactionManagerPlatform extends AbstractJtaPlatform implements JtaPlatform {

    private static final long serialVersionUID = 8676743510117311360L;

    private volatile JtaTransactionManager transactionManager;

    public LocalTransactionManagerPlatform() {

        JtaTransactionManager tm = HibernateEntityManagerFactoryBean.getConfigurationTransactionManager();
        if (tm == null) {
            tm = HibernateSessionFactoryBean.getConfigurationTransactionManager();
        }

        if (tm == null) {
            throw new IllegalStateException(
                    "No JTA TransactionManager found - "
                            + "'hibernate.transaction.jta.platform' property must be set on the containing LocalSessionFactoryBean or LocalEntityManagerFactoryBean as appropriate");
        }

        this.transactionManager = tm;
    }

We've solved the integration part. So now we only need configure the EntityManager using our specialised subclass, injecting the transaction manager into it, and configuring our JtaPlatform as a Hibernate property. The configuration is completely portable - we just need to change the specification of our transaction manager bean.

Unfriendly Containers

Some containers (one in particular) stand out as being unfriendly to this strategy because although they implement JtaTransactionManager as part of the Spring provided class, their getTransactionManager()  method doesn't return anything meaningful. IBM's WebSphere is the standout example as they fulfill the JTA contracts in Spring using the proprietary Unit Of Work API. To get this environment working with our approach we need to extend the Spring provided class to return a valid transaction manager instance. What we actually will do is return an adapter that fulfills (enough) of the JTA API by delegating to the WebSphere Unit of Work API. A pure Hibernate solution for this is documented at itdevworld. Its straightforward to adapt this approach by subclassing WebSphereUowTransactionManager. Here's the code:

public class SpringWebSphereUowTransactionManager extends WebSphereUowTransactionManager {

    private static final long serialVersionUID = 4838070722625854290L;

    private static final String UOW_SYNCHRONIZATION_REGISTRY_JNDINAME = "java:comp/websphere/UOWSynchronizationRegistry";
    private static final String USER_TRANASCTION_JNDINAME = "java:comp/UserTransaction";

    private static final Field UOW_FIELD;

    static {
        try {
            UOW_FIELD = WebSphereUowTransactionManager.class.getDeclaredField("uowManager");
            UOW_FIELD.setAccessible(true);
        } catch (SecurityException e) {
            throw new IllegalStateException(
                    "Not permitted to access WebSphereUowTransactionManager: " + e.getMessage(), e);
        } catch (NoSuchFieldException e) {
            throw new IllegalStateException("Could not find WebSphereUowTransactionManager: " + e.getMessage(), e);
        }
    }

    /**
     * Creates a new instance
     */
    public SpringWebSphereUowTransactionManager() {
        super();
    }

    @Override
    public void afterPropertiesSet() throws TransactionSystemException {
        super.afterPropertiesSet();
        setTransactionManager(new TransactionManagerAdapter(getJndiTemplate(), retrieveUowManager()));
        setUserTransactionName(USER_TRANASCTION_JNDINAME);
    }

    private Object retrieveUowManager() {
        try {
            Object uowManager = UOW_FIELD.get(this);
            return uowManager;
        } catch (SecurityException e) {
            throw new IllegalStateException(
                    "Not permitted to access WebSphereUowTransactionManager: " + e.getMessage(), e);
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException("Unexpected argument accessing WebSphereUowTransactionManager: "
                    + e.getMessage(), e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception accessing WebSphereUowTransactionManager: "
                    + e.getMessage(), e);
        }
    }

    /**
     * An adapter that fulfils the JTA {@link TransactionManager} by delegating to the WebSphereUOWTransactionManager
     */
    public static class TransactionManagerAdapter implements TransactionManager {

        private final JndiTemplate jndiTemplate;

        private final Object uowManager;
        private final Class<?> uowManagerClass;

        private final Object uowSynchronizationRegistry;
        private final Class<?> uowSynchronizationRegistryClass;

        private final Method registerSynchronizationMethod;
        private final Method setRollbackOnlyMethod;

        private final Class<?> extendedJTATransactionClass;
        private final Method getLocalIdMethod;

        /**
         * Create a new instance
         * @param jndiTemplate An instance of Spring's JndiTemplate to use to look up resources
         * @param uowManager UOWManager to use
         */
        private TransactionManagerAdapter(JndiTemplate jndiTemplate, Object uowManager) {

            try {
                this.uowManagerClass = Class.forName("com.ibm.ws.uow.UOWManager");

                this.uowSynchronizationRegistry = jndiTemplate.lookup(UOW_SYNCHRONIZATION_REGISTRY_JNDINAME);
                this.uowSynchronizationRegistryClass = Class
                        .forName("com.ibm.websphere.uow.UOWSynchronizationRegistry");

                this.registerSynchronizationMethod = uowSynchronizationRegistryClass.getMethod(
                        "registerInterposedSynchronization", new Class[] { Synchronization.class });
                this.setRollbackOnlyMethod = uowManagerClass.getMethod("setRollbackOnly", new Class[] {});

                this.extendedJTATransactionClass = Class
                        .forName("com.ibm.websphere.jtaextensions.ExtendedJTATransaction");
                this.getLocalIdMethod = extendedJTATransactionClass.getMethod("getLocalId", (Class[]) null);

            } catch (ClassNotFoundException e) {
                throw new IllegalStateException("Could not find required WebSphere class: " + e.getMessage(), e);
            } catch (NoSuchMethodException e) {
                throw new IllegalStateException("Could not find required method: " + e.getMessage(), e);
            } catch (NamingException e) {
                throw new IllegalStateException("Problem accessing JNDI: " + e.getMessage(), e);
            }

            this.jndiTemplate = jndiTemplate;
            this.uowManager = uowManager;
        }

        @Override
        public void begin() {
            throw new UnsupportedOperationException("begin() is not supported");
        }

        @Override
        public void commit() {
            throw new UnsupportedOperationException("commit() is not supported");
        }

        @Override
        public int getStatus() {
            throw new UnsupportedOperationException("getStatus() is not supported");
        }

        @Override
        public void resume(Transaction txn) {
            throw new UnsupportedOperationException("resume() is not supported");
        }

        @Override
        public void rollback() {
            throw new UnsupportedOperationException("rollback() is not supported");
        }

        @Override
        public void setTransactionTimeout(int i) {
            throw new UnsupportedOperationException("setTransactionTimeout() is not supported");
        }

        @Override
        public Transaction suspend() {
            throw new UnsupportedOperationException("suspend() is not supported");
        }

        @Override
        public void setRollbackOnly() throws IllegalStateException {
            try {
                setRollbackOnlyMethod.invoke(uowManager, new Object[] {});
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Could not access setRollbackOnly() on UOWManager: " + e.getMessage(),
                        e);
            } catch (InvocationTargetException e) {
                throw new IllegalStateException("Could not invoke setRollbackOnly() on UOWManager: " + e.getMessage(),
                        e);
            }
        }

        @Override
        public Transaction getTransaction() {
            return new TransactionAdapter(jndiTemplate);
        }

        /**
         * An adapter that fulfils the JTA transaction interface.
         */
        public class TransactionAdapter implements Transaction {

            private final Object extendedJTATransaction;

            /**
             * Creates a new instance
             * @param template The JndiTemplate
             */
            private TransactionAdapter(JndiTemplate template) {
                try {
                    extendedJTATransaction = template.lookup("java:comp/websphere/ExtendedJTATransaction");

                } catch (NamingException e) {
                    throw new IllegalStateException("Could not find ExtendedJTATransaction in JNDI: " + e.getMessage(),
                            e);
                }
            }

            @Override
            public void registerSynchronization(final Synchronization synchronization) {

                try {
                    registerSynchronizationMethod.invoke(uowSynchronizationRegistry, new Object[] { synchronization });
                } catch (IllegalArgumentException e) {
                    throw new IllegalStateException("Unexpected argument accessing UOWSynchronizationRegistry: "
                            + e.getMessage(), e);
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException("Unexpected exception accessing UOWSynchronizationRegistry: "
                            + e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new IllegalStateException(
                            "Could not invoke registerSynchronization() on UOWSynchronizationRegistry: "
                                    + e.getMessage(), e);
                }
            }

            @Override
            public void commit() {
                throw new UnsupportedOperationException("commit() is not supported");
            }

            @Override
            public boolean delistResource(XAResource resource, int i) {
                throw new UnsupportedOperationException("delistResource() is not supported");
            }

            @Override
            public boolean enlistResource(XAResource resource) {
                throw new UnsupportedOperationException("enlistResource() is not supported");
            }

            @Override
            public int getStatus() {
                if (0 == getLocalId()) {
                    return Status.STATUS_NO_TRANSACTION;
                } else {
                    return Status.STATUS_ACTIVE;
                }
            }

            @Override
            public void rollback() throws IllegalStateException, SystemException {
                throw new UnsupportedOperationException("rollback() is not supported");
            }

            @Override
            public void setRollbackOnly() {
                try {
                    setRollbackOnlyMethod.invoke(uowManager, new Object[] {});
                } catch (IllegalArgumentException e) {
                    throw new IllegalStateException("Unexpected argument accessing UOWManager: " + e.getMessage(), e);
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException("Unexpected exception accessing UOWManager: " + e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new IllegalStateException("Could not invoke setRollbackOnly() on UOWManager: "
                            + e.getMessage(), e);
                }
            }

            @Override
            public int hashCode() {
                return getLocalId();
            }

            @Override
            public boolean equals(Object other) {
                if (!(other instanceof TransactionAdapter))
                    return false;
                TransactionAdapter that = (TransactionAdapter) other;
                return getLocalId() == that.getLocalId();
            }

            private int getLocalId() {
                try {
                    return ((Integer) (getLocalIdMethod.invoke(extendedJTATransaction, (Object[]) null))).intValue();
                } catch (IllegalArgumentException e) {
                    throw new IllegalStateException("Unexpected argument accessing ExtendedJTATransaction: "
                            + e.getMessage(), e);
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException("Unexpected exception accessing ExtendedJTATransaction: "
                            + e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    throw new IllegalStateException("Could not invoke getLocalId() on ExtendedJTATransaction: "
                            + e.getMessage(), e);
                }
            }

        }
    }
}

Unit Testing

I've still not discussed how unit testing can be performed with JTA. In the past this used to be quite tricky, but now its straightforward using JBoss Narayana (formerly JBoss Transactions, formely Arjuna).

Narayana needs to be added as a test build dependency:

            <dependency>
                <groupId>org.jboss.narayana.jta</groupId>
                <artifactId>narayana-jta</artifactId>
                <version>5.0.1.Final</version>
                <scope>test</scope>
            </dependency>

 And the Spring Transaction manager that uses Narayana needs to be declared:

<bean id="jbossTransactionManager" class="com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple">
        <property name="transactionTimeout" value="300" />
    </bean>

    <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="transactionManager">
            <ref bean="jbossTransactionManager" />
        </property>
        <property name="allowCustomIsolationLevels" value="true" />
    </bean>

The configuration is ready for Unit Testing.

I've had to solve this problem many times - I hope the solution is useful.

You'll find the full code in the usertype.spi module in the package org.jadira.usertype.spi.jta. Do let me know if you can suggest improvements or find problems with it.

PrintView Printer Friendly Version

EmailEmail Article to Friend

References (2)

References allow you to track sources for this article, as well as articles that were written in response to this article.

Reader Comments

There are no comments for this journal entry. To create a new comment, use the form below.

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>