com.taobao.itest.listener.TransactionalListener.java Source code

Java tutorial

Introduction

Here is the source code for com.taobao.itest.listener.TransactionalListener.java

Source

/*
 * Copyright 2002-2008 the original author or authors.
 * Copyright 2011 Alibaba Group Holding Limited.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.taobao.itest.listener;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.test.annotation.NotTransactional;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.test.context.transaction.TransactionConfigurationAttributes;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.DelegatingTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.util.Assert;

import com.taobao.itest.core.AbstractTestListener;
import com.taobao.itest.core.TestContext;
import com.taobao.itest.spring.context.SpringContextManager;

/**
 * <p>
 * <code>TestExecutionListener</code> which provides support for executing tests
 * within transactions by using
 * {@link org.springframework.transaction.annotation.Transactional
 * @Transactional} and {@link NotTransactional @NotTransactional} annotations.
 * </p>
 * <p>
 * Changes to the database during a test run with &#064;Transactional will be
 * run within a transaction that will, by default, be automatically
 * <em>rolled back</em> after completion of the test; whereas, changes to the
 * database during a test run with &#064;NotTransactional will
 * <strong>not</strong> be run within a transaction. Similarly, test methods
 * that are not annotated with either &#064;Transactional (at the class or
 * method level) or &#064;NotTransactional will not be run within a transaction.
 * </p>
 * <p>
 * Transactional commit and rollback behavior can be configured via the
 * class-level {@link TransactionConfiguration @TransactionConfiguration} and
 * method-level {@link Rollback @Rollback} annotations.
 * {@link TransactionConfiguration @TransactionConfiguration} also provides
 * configuration of the bean name of the {@link PlatformTransactionManager} that
 * is to be used to drive transactions.
 * </p>
 * <p>
 * When executing transactional tests, it is sometimes useful to be able execute
 * certain <em>set up</em> or <em>tear down</em> code outside of a transaction.
 * <code>TransactionalTestExecutionListener</code> provides such support for
 * methods annotated with {@link BeforeTransaction @BeforeTransaction} and
 * {@link AfterTransaction @AfterTransaction}.
 * </p>
 * 
 * @author Sam Brannen
 * @author Juergen Hoeller
 * @since 2.5
 * @see TransactionConfiguration
 * @see org.springframework.transaction.annotation.Transactional
 * @see org.springframework.test.annotation.NotTransactional
 * @see org.springframework.test.annotation.Rollback
 * @see BeforeTransaction
 * @see AfterTransaction
 */
public class TransactionalListener extends AbstractTestListener {

    private static final Log logger = LogFactory.getLog(TransactionalListener.class);

    protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();

    private TransactionConfigurationAttributes configAttributes;

    private volatile int transactionsStarted = 0;

    private final Map<Method, TransactionContext> transactionContextCache = Collections
            .synchronizedMap(new IdentityHashMap<Method, TransactionContext>());

    /**
     * If the test method of the supplied {@link TestContext test context} is
     * configured to run within a transaction, this method will run
     * {@link BeforeTransaction @BeforeTransaction methods} and start a new
     * transaction.
     * <p>
     * Note that if a {@link BeforeTransaction @BeforeTransaction method} fails,
     * remaining {@link BeforeTransaction @BeforeTransaction methods} will not
     * be invoked, and a transaction will not be started.
     * 
     * @see org.springframework.transaction.annotation.Transactional
     * @see org.springframework.test.annotation.NotTransactional
     */
    @SuppressWarnings("serial")
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        final Method testMethod = testContext.getTestMethod();
        Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");

        if (this.transactionContextCache.remove(testMethod) != null) {
            throw new IllegalStateException("Cannot start new transaction without ending existing transaction: "
                    + "Invoke endTransaction() before startNewTransaction().");
        }

        if (testMethod.isAnnotationPresent(NotTransactional.class)) {
            return;
        }

        TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod,
                testContext.getTestClass());
        TransactionDefinition transactionDefinition = null;
        if (transactionAttribute != null) {
            transactionDefinition = new DelegatingTransactionAttribute(transactionAttribute) {
                public String getName() {
                    return testMethod.getName();
                }
            };
        }

        if (transactionDefinition != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Explicit transaction definition [" + transactionDefinition
                        + "] found for test context [" + testContext + "]");
            }
            TransactionContext txContext = new TransactionContext(getTransactionManager(testContext),
                    transactionDefinition);
            runBeforeTransactionMethods(testContext);
            startNewTransaction(testContext, txContext);
            this.transactionContextCache.put(testMethod, txContext);
        }
    }

    /**
     * If a transaction is currently active for the test method of the supplied
     * {@link TestContext test context}, this method will end the transaction
     * and run {@link AfterTransaction @AfterTransaction methods}.
     * <p>
     * {@link AfterTransaction @AfterTransaction methods} are guaranteed to be
     * invoked even if an error occurs while ending the transaction.
     */
    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        Method testMethod = testContext.getTestMethod();
        Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");

        // If the transaction is still active...
        TransactionContext txContext = this.transactionContextCache.remove(testMethod);
        if (txContext != null && !txContext.transactionStatus.isCompleted()) {
            try {
                endTransaction(testContext, txContext);
            } finally {
                runAfterTransactionMethods(testContext);
            }
        }
    }

    /**
     * Run all {@link BeforeTransaction @BeforeTransaction methods} for the
     * specified {@link TestContext test context}. If one of the methods fails,
     * however, the caught exception will be rethrown in a wrapped
     * {@link RuntimeException}, and the remaining methods will
     * <strong>not</strong> be given a chance to execute.
     * 
     * @param testContext
     *            the current test context
     */
    protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
        try {
            List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class);
            Collections.reverse(methods);
            for (Method method : methods) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Executing @BeforeTransaction method [" + method + "] for test context ["
                            + testContext + "]");
                }
                method.invoke(testContext.getTestInstance());
            }
        } catch (InvocationTargetException ex) {
            logger.error("Exception encountered while executing @BeforeTransaction methods for test context ["
                    + testContext + "]", ex.getTargetException());
            rethrowException(ex.getTargetException());
        }
    }

    /**
     * Run all {@link AfterTransaction @AfterTransaction methods} for the
     * specified {@link TestContext test context}. If one of the methods fails,
     * the caught exception will be logged as an error, and the remaining
     * methods will be given a chance to execute. After all methods have
     * executed, the first caught exception, if any, will be rethrown.
     * 
     * @param testContext
     *            the current test context
     */
    protected void runAfterTransactionMethods(TestContext testContext) throws Exception {
        Throwable afterTransactionException = null;

        List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class);
        for (Method method : methods) {
            try {
                if (logger.isDebugEnabled()) {
                    logger.debug("Executing @AfterTransaction method [" + method + "] for test context ["
                            + testContext + "]");
                }
                method.invoke(testContext.getTestInstance());
            } catch (InvocationTargetException ex) {
                Throwable targetException = ex.getTargetException();
                if (afterTransactionException == null) {
                    afterTransactionException = targetException;
                }
                logger.error("Exception encountered while executing @AfterTransaction method [" + method
                        + "] for test context [" + testContext + "]", targetException);
            } catch (Exception ex) {
                if (afterTransactionException == null) {
                    afterTransactionException = ex;
                }
                logger.error("Exception encountered while executing @AfterTransaction method [" + method
                        + "] for test context [" + testContext + "]", ex);
            }
        }

        if (afterTransactionException != null) {
            rethrowException(afterTransactionException);
        }
    }

    /**
     * Start a new transaction for the supplied {@link TestContext test context}
     * .
     * <p>
     * Only call this method if {@link #endTransaction} has been called or if no
     * transaction has been previously started.
     * 
     * @param testContext
     *            the current test context
     * @throws TransactionException
     *             if starting the transaction fails
     * @throws Exception
     *             if an error occurs while retrieving the transaction manager
     */
    private void startNewTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
        txContext.startTransaction();
        ++this.transactionsStarted;
        if (logger.isInfoEnabled()) {
            logger.info("Began transaction (" + this.transactionsStarted + "): transaction manager ["
                    + txContext.transactionManager + "]; rollback [" + isRollback(testContext) + "]");
        }
    }

    /**
     * Immediately force a <em>commit</em> or <em>rollback</em> of the
     * transaction for the supplied {@link TestContext test context}, according
     * to the commit and rollback flags.
     * 
     * @param testContext
     *            the current test context
     * @throws Exception
     *             if an error occurs while retrieving the transaction manager
     */
    private void endTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
        boolean rollback = isRollback(testContext);
        if (logger.isTraceEnabled()) {
            logger.trace("Ending transaction for test context [" + testContext + "]; transaction manager ["
                    + txContext.transactionStatus + "]; rollback [" + rollback + "]");
        }
        txContext.endTransaction(rollback);
        if (logger.isInfoEnabled()) {
            logger.info((rollback ? "Rolled back" : "Committed")
                    + " transaction after test execution for test context [" + testContext + "]");
        }
    }

    /**
     * Get the {@link PlatformTransactionManager transaction manager} to use for
     * the supplied {@link TestContext test context}.
     * 
     * @param testContext
     *            the test context for which the transaction manager should be
     *            retrieved
     * @return the transaction manager to use, or <code>null</code> if not found
     * @throws BeansException
     *             if an error occurs while retrieving the transaction manager
     */
    protected final PlatformTransactionManager getTransactionManager(TestContext testContext) {
        if (this.configAttributes == null) {
            this.configAttributes = retrieveTransactionConfigurationAttributes(testContext.getTestClass());
        }
        String transactionManagerName = this.configAttributes.getTransactionManagerName();
        try {
            return (PlatformTransactionManager) SpringContextManager.getApplicationContext()
                    .getBean(transactionManagerName, PlatformTransactionManager.class);
        } catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Caught exception while retrieving transaction manager with bean name ["
                        + transactionManagerName + "] for test context [" + testContext + "]", ex);
            }
            throw ex;
        }
    }

    /**
     * Determine whether or not to rollback transactions by default for the
     * supplied {@link TestContext test context}.
     * 
     * @param testContext
     *            the test context for which the default rollback flag should be
     *            retrieved
     * @return the <em>default rollback</em> flag for the supplied test context
     * @throws Exception
     *             if an error occurs while determining the default rollback
     *             flag
     */
    protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
        return retrieveTransactionConfigurationAttributes(testContext.getTestClass()).isDefaultRollback();
    }

    /**
     * Determine whether or not to rollback transactions for the supplied
     * {@link TestContext test context} by taking into consideration the
     * {@link #isDefaultRollback(TestContext) default rollback} flag and a
     * possible method-level override via the {@link Rollback} annotation.
     * 
     * @param testContext
     *            the test context for which the rollback flag should be
     *            retrieved
     * @return the <em>rollback</em> flag for the supplied test context
     * @throws Exception
     *             if an error occurs while determining the rollback flag
     */
    protected final boolean isRollback(TestContext testContext) throws Exception {
        boolean rollback = isDefaultRollback(testContext);
        Rollback rollbackAnnotation = testContext.getTestMethod().getAnnotation(Rollback.class);
        if (rollbackAnnotation != null) {
            boolean rollbackOverride = rollbackAnnotation.value();
            if (logger.isDebugEnabled()) {
                logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback ["
                        + rollback + "] for test context [" + testContext + "]");
            }
            rollback = rollbackOverride;
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("No method-level @Rollback override: using default rollback [" + rollback
                        + "] for test context [" + testContext + "]");
            }
        }
        return rollback;
    }

    /**
     * Gets all superclasses of the supplied {@link Class class}, including the
     * class itself. The ordering of the returned list will begin with the
     * supplied class and continue up the class hierarchy.
     * <p>
     * Note: This code has been borrowed from
     * {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and
     * adapted.
     * 
     * @param clazz
     *            the class for which to retrieve the superclasses.
     * @return all superclasses of the supplied class.
     */
    private List<Class<?>> getSuperClasses(Class<?> clazz) {
        ArrayList<Class<?>> results = new ArrayList<Class<?>>();
        Class<?> current = clazz;
        while (current != null) {
            results.add(current);
            current = current.getSuperclass();
        }
        return results;
    }

    /**
     * Gets all methods in the supplied {@link Class class} and its superclasses
     * which are annotated with the supplied <code>annotationType</code> but
     * which are not <em>shadowed</em> by methods overridden in subclasses.
     * <p>
     * Note: This code has been borrowed from
     * {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)}
     * and adapted.
     * 
     * @param clazz
     *            the class for which to retrieve the annotated methods
     * @param annotationType
     *            the annotation type for which to search
     * @return all annotated methods in the supplied class and its superclasses
     */
    private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
        List<Method> results = new ArrayList<Method>();
        for (Class<?> eachClass : getSuperClasses(clazz)) {
            Method[] methods = eachClass.getDeclaredMethods();
            for (Method eachMethod : methods) {
                Annotation annotation = eachMethod.getAnnotation(annotationType);
                if (annotation != null && !isShadowed(eachMethod, results)) {
                    results.add(eachMethod);
                }
            }
        }
        return results;
    }

    /**
     * Determines if the supplied {@link Method method} is <em>shadowed</em> by
     * a method in supplied {@link List list} of previous methods.
     * <p>
     * Note: This code has been borrowed from
     * {@link org.junit.internal.runners.TestClass#isShadowed(Method,List)}.
     * 
     * @param method
     *            the method to check for shadowing
     * @param previousMethods
     *            the list of methods which have previously been processed
     * @return <code>true</code> if the supplied method is shadowed by a method
     *         in the <code>previousMethods</code> list
     */
    private boolean isShadowed(Method method, List<Method> previousMethods) {
        for (Method each : previousMethods) {
            if (isShadowed(method, each)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if the supplied {@link Method current method} is
     * <em>shadowed</em> by a {@link Method previous method}.
     * <p>
     * Note: This code has been borrowed from
     * {@link org.junit.internal.runners.TestClass#isShadowed(Method,Method)}.
     * 
     * @param current
     *            the current method
     * @param previous
     *            the previous method
     * @return <code>true</code> if the previous method shadows the current one
     */
    private boolean isShadowed(Method current, Method previous) {
        if (!previous.getName().equals(current.getName())) {
            return false;
        }
        if (previous.getParameterTypes().length != current.getParameterTypes().length) {
            return false;
        }
        for (int i = 0; i < previous.getParameterTypes().length; i++) {
            if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * <p>
     * Retrieves the {@link TransactionConfigurationAttributes} for the
     * specified {@link Class class} which may optionally declare or inherit a
     * {@link TransactionConfiguration @TransactionConfiguration}. If a
     * {@link TransactionConfiguration} annotation is not present for the
     * supplied class, the <em>default values</em> for attributes defined in
     * {@link TransactionConfiguration} will be used instead.
     * 
     * @param clazz
     *            the Class object corresponding to the test class for which the
     *            configuration attributes should be retrieved
     * @return a new TransactionConfigurationAttributes instance
     */
    private TransactionConfigurationAttributes retrieveTransactionConfigurationAttributes(Class<?> clazz) {
        Class<TransactionConfiguration> annotationType = TransactionConfiguration.class;
        TransactionConfiguration config = clazz.getAnnotation(annotationType);
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]");
        }

        String transactionManagerName;
        boolean defaultRollback;
        if (config != null) {
            transactionManagerName = config.transactionManager();
            defaultRollback = config.defaultRollback();
        } else {
            transactionManagerName = (String) getDefaultValue(annotationType, "transactionManager");
            defaultRollback = (Boolean) getDefaultValue(annotationType, "defaultRollback");
        }

        TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes(
                transactionManagerName, defaultRollback);
        if (logger.isDebugEnabled()) {
            logger.debug("Retrieved TransactionConfigurationAttributes [" + configAttributes + "] for class ["
                    + clazz + "]");
        }
        return configAttributes;
    }

    /**
     * Internal context holder for a specific test method.
     */
    private static class TransactionContext {

        private final PlatformTransactionManager transactionManager;

        private final TransactionDefinition transactionDefinition;

        private TransactionStatus transactionStatus;

        public TransactionContext(PlatformTransactionManager transactionManager,
                TransactionDefinition transactionDefinition) {
            this.transactionManager = transactionManager;
            this.transactionDefinition = transactionDefinition;
        }

        public void startTransaction() {
            this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
        }

        public void endTransaction(boolean rollback) {
            if (rollback) {
                this.transactionManager.rollback(this.transactionStatus);
            } else {
                this.transactionManager.commit(this.transactionStatus);
            }
        }
    }

    /**
     * Obtain a PlatformTransactionManager from the given BeanFactory, matching
     * the given qualifier.
     * 
     * @param beanFactory
     *            the BeanFactory to get the PlatformTransactionManager bean
     *            from
     * @param qualifier
     *            the qualifier for selecting between multiple
     *            PlatformTransactionManager matches
     * @return the chosen PlatformTransactionManager (never <code>null</code>)
     * @throws IllegalStateException
     *             if no matching PlatformTransactionManager bean found
     */
    @SuppressWarnings("unused")
    private static PlatformTransactionManager getTransactionManager(BeanFactory beanFactory, String qualifier) {
        if (beanFactory instanceof ConfigurableListableBeanFactory) {
            // Full qualifier matching supported.
            return getTransactionManager((ConfigurableListableBeanFactory) beanFactory, qualifier);
        } else if (beanFactory.containsBean(qualifier)) {
            // Fallback: PlatformTransactionManager at least found by bean name.
            return (PlatformTransactionManager) beanFactory.getBean(qualifier, PlatformTransactionManager.class);
        } else {
            throw new IllegalStateException("No matching PlatformTransactionManager bean found for bean name '"
                    + qualifier + "'! (Note: Qualifier matching not supported because given BeanFactory does not "
                    + "implement ConfigurableListableBeanFactory.)");
        }
    }

    /**
     * Rethrow the given {@link Throwable exception}, which is presumably the
     * <em>target exception</em> of an {@link InvocationTargetException}. Should
     * only be called if no checked exception is expected to be thrown by the
     * target method.
     * <p>
     * Rethrows the underlying exception cast to an {@link Exception} or
     * {@link Error} if appropriate; otherwise, throws an
     * {@link IllegalStateException}.
     * 
     * @param ex
     *            the exception to rethrow
     * @throws Exception
     *             the rethrown exception (in case of a checked exception)
     */
    public static void rethrowException(Throwable ex) throws Exception {
        if (ex instanceof Exception) {
            throw (Exception) ex;
        }
        if (ex instanceof Error) {
            throw (Error) ex;
        }
        throw new IllegalStateException("Unexpected exception thrown", ex);
    }

    /**
     * Retrieve the <em>default value</em> of a named Annotation attribute,
     * given the {@link Class annotation type}.
     * 
     * @param annotationType
     *            the <em>annotation type</em> for which the default value
     *            should be retrieved
     * @param attributeName
     *            the name of the attribute value to retrieve.
     * @return the default value of the named attribute, or <code>null</code> if
     *         not found
     * @see #getDefaultValue(Annotation, String)
     */
    public static Object getDefaultValue(Class<? extends Annotation> annotationType, String attributeName) {
        try {
            Method method = annotationType.getDeclaredMethod(attributeName, new Class[0]);
            return method.getDefaultValue();
        } catch (Exception ex) {
            return null;
        }
    }

}