@Bean(name = AnnotationConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public AsyncAnnotationBeanPostProcessor asyncAdvisor() { Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected"); AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor(); Class<? extends Annotation> customAsyncAnnotation = enableAsync.getClass("annotation"); if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) { bpp.setAsyncAnnotationType(customAsyncAnnotation); } if (this.executor != null) { bpp.setExecutor(this.executor); } bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass")); bpp.setOrder(this.enableAsync.<Integer>getNumber("order")); return bpp; }
/** Match the given qualifier annotation against the candidate bean definition. */ protected boolean checkQualifier( BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) { Class<? extends Annotation> type = annotation.annotationType(); RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition(); AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName()); if (qualifier == null) { qualifier = bd.getQualifier(ClassUtils.getShortName(type)); } if (qualifier == null) { Annotation targetAnnotation = null; if (bd.getResolvedFactoryMethod() != null) { targetAnnotation = AnnotationUtils.getAnnotation(bd.getResolvedFactoryMethod(), type); } if (targetAnnotation == null) { // look for matching annotation on the target class if (this.beanFactory != null) { Class<?> beanType = this.beanFactory.getType(bdHolder.getBeanName()); if (beanType != null) { targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type); } } if (targetAnnotation == null && bd.hasBeanClass()) { targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type); } } if (targetAnnotation != null && targetAnnotation.equals(annotation)) { return true; } } Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation); if (attributes.isEmpty() && qualifier == null) { // if no attributes, the qualifier must be present return false; } for (Map.Entry<String, Object> entry : attributes.entrySet()) { String attributeName = entry.getKey(); Object expectedValue = entry.getValue(); Object actualValue = null; // check qualifier first if (qualifier != null) { actualValue = qualifier.getAttribute(attributeName); } if (actualValue == null) { // fall back on bean definition attribute actualValue = bd.getAttribute(attributeName); } if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) && expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)) { // fall back on bean name (or alias) match continue; } if (actualValue == null && qualifier != null) { // fall back on default, but only if the qualifier is present actualValue = AnnotationUtils.getDefaultValue(annotation, attributeName); } if (actualValue != null) { actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); } if (!expectedValue.equals(actualValue)) { return false; } } return true; }
/** * {@code TestExecutionListener} that provides support for executing tests within transactions by * honoring the {@link org.springframework.transaction.annotation.Transactional @Transactional} * annotation. Expects a {@link PlatformTransactionManager} bean to be defined in the Spring {@link * ApplicationContext} for the test. * * <p>Changes to the database during a test that is run with {@code @Transactional} will be run * within a transaction that will, by default, be automatically <em>rolled back</em> after * completion of the test. Test methods that are not annotated with {@code @Transactional} (at the * class or method level) will not be run within a transaction. * * <p>Transactional commit and rollback behavior can be configured via the class-level {@link * TransactionConfiguration @TransactionConfiguration} and method-level {@link Rollback @Rollback} * annotations. * * <p>In case there are multiple instances of {@code PlatformTransactionManager} within the test's * {@code ApplicationContext}, {@code @TransactionConfiguration} supports configuring the bean name * of the {@code PlatformTransactionManager} that should be used to drive transactions. * Alternatively, {@link TransactionManagementConfigurer} can be implemented in an {@link * org.springframework.context.annotation.Configuration @Configuration} class. * * <p>When executing transactional tests, it is sometimes useful to be able to execute certain * <em>set up</em> or <em>tear down</em> code outside of a transaction. {@code * TransactionalTestExecutionListener} provides such support for methods annotated with {@link * BeforeTransaction @BeforeTransaction} and {@link AfterTransaction @AfterTransaction}. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 * @see TransactionConfiguration * @see TransactionManagementConfigurer * @see org.springframework.transaction.annotation.Transactional * @see org.springframework.test.annotation.Rollback * @see BeforeTransaction * @see AfterTransaction */ public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); private static final String DEFAULT_TRANSACTION_MANAGER_NAME = (String) AnnotationUtils.getDefaultValue(TransactionConfiguration.class, "transactionManager"); private static final Boolean DEFAULT_DEFAULT_ROLLBACK = (Boolean) AnnotationUtils.getDefaultValue(TransactionConfiguration.class, "defaultRollback"); protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); private final Map<Method, TransactionContext> transactionContextCache = new ConcurrentHashMap<Method, TransactionContext>(8); private TransactionConfigurationAttributes configurationAttributes; private volatile int transactionsStarted = 0; /** * 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 {@code @BeforeTransaction} method fails, any remaining * {@code @BeforeTransaction} methods will not be invoked, and a transaction will not be started. * * @see org.springframework.transaction.annotation.Transactional * @see #getTransactionManager(TestContext, String) */ @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()."); } PlatformTransactionManager tm = null; TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testContext.getTestClass()); if (transactionAttribute != null) { transactionAttribute = new DelegatingTransactionAttribute(transactionAttribute) { @Override public String getName() { return testMethod.getName(); } }; if (logger.isDebugEnabled()) { logger.debug( "Explicit transaction definition [" + transactionAttribute + "] found for test context " + testContext); } if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { return; } tm = getTransactionManager(testContext, transactionAttribute.getQualifier()); } if (tm != null) { TransactionContext txContext = new TransactionContext(tm, transactionAttribute); 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>{@code @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()); ReflectionUtils.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) { ReflectionUtils.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} and {@code qualifier}. * * <p>Delegates to {@link #getTransactionManager(TestContext)} if the supplied {@code qualifier} * is {@code null} or empty. * * @param testContext the test context for which the transaction manager should be retrieved * @param qualifier the qualifier for selecting between multiple bean matches; may be {@code null} * or empty * @return the transaction manager to use, or {@code null} if not found * @throws BeansException if an error occurs while retrieving the transaction manager * @see #getTransactionManager(TestContext) */ protected final PlatformTransactionManager getTransactionManager( TestContext testContext, String qualifier) { // look up by type and qualifier from @Transactional if (StringUtils.hasText(qualifier)) { try { // Use autowire-capable factory in order to support extended qualifier // matching (only exposed on the internal BeanFactory, not on the // ApplicationContext). BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); return BeanFactoryAnnotationUtils.qualifiedBeanOfType( bf, PlatformTransactionManager.class, qualifier); } catch (RuntimeException ex) { if (logger.isWarnEnabled()) { logger.warn( "Caught exception while retrieving transaction manager for test context " + testContext + " and qualifier [" + qualifier + "]", ex); } throw ex; } } // else return getTransactionManager(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} if not found * @throws BeansException if an error occurs while retrieving the transaction manager * @see #getTransactionManager(TestContext, String) */ protected final PlatformTransactionManager getTransactionManager(TestContext testContext) { BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName(); try { // look up by type and explicit name from @TransactionConfiguration if (StringUtils.hasText(tmName) && !DEFAULT_TRANSACTION_MANAGER_NAME.equals(tmName)) { return bf.getBean(tmName, PlatformTransactionManager.class); } if (bf instanceof ListableBeanFactory) { ListableBeanFactory lbf = (ListableBeanFactory) bf; // look up single bean by type Map<String, PlatformTransactionManager> txMgrs = BeanFactoryUtils.beansOfTypeIncludingAncestors(lbf, PlatformTransactionManager.class); if (txMgrs.size() == 1) { return txMgrs.values().iterator().next(); } // look up single TransactionManagementConfigurer Map<String, TransactionManagementConfigurer> configurers = BeanFactoryUtils.beansOfTypeIncludingAncestors( lbf, TransactionManagementConfigurer.class); if (configurers.size() > 1) { throw new IllegalStateException( "Only one TransactionManagementConfigurer may exist in the ApplicationContext"); } if (configurers.size() == 1) { return configurers.values().iterator().next().annotationDrivenTransactionManager(); } } // look up by type and default name from @TransactionConfiguration return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn( "Caught exception while retrieving transaction manager 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 retrieveConfigurationAttributes(testContext).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} 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} if the supplied method is shadowed by a method in the {@code * previousMethods} 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} 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; } /** * Retrieves the {@link TransactionConfigurationAttributes} for the specified {@link Class class} * which may optionally declare or inherit {@link TransactionConfiguration * @TransactionConfiguration}. If {@code @TransactionConfiguration} is not present for the * supplied class, the <em>default values</em> for attributes defined in * {@code @TransactionConfiguration} will be used instead. * * @param testContext the test context for which the configuration attributes should be retrieved * @return a new TransactionConfigurationAttributes instance */ private TransactionConfigurationAttributes retrieveConfigurationAttributes( TestContext testContext) { if (this.configurationAttributes == null) { Class<?> clazz = testContext.getTestClass(); TransactionConfiguration config = clazz.getAnnotation(TransactionConfiguration.class); 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 = DEFAULT_TRANSACTION_MANAGER_NAME; defaultRollback = DEFAULT_DEFAULT_ROLLBACK; } TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes(transactionManagerName, defaultRollback); if (logger.isDebugEnabled()) { logger.debug( "Retrieved TransactionConfigurationAttributes " + configAttributes + " for class [" + clazz + "]"); } this.configurationAttributes = configAttributes; } return this.configurationAttributes; } /** 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); } } } }