Spring is a widely used framework today, bringing many powerfull features and extensions to the Java core stack. However most of people tend to use these features without understanding their underlying mechanism.
Since there is no “magic” in real life, we are going to dig into some Spring features related to Transaction and Database in this serie of articles.
This first article is dealing with the famous @Transactional annotation, saving the developers the burden of managing low level transaction code.
The second article is available here:
Spring @PersistenceContext/@PersistenceUnit explained
Note: the following code analysis was done with Spring 3.0.5 official release We only focus on the J2SE environment (no EJB) and on the JPA API for database management. However, the code analyzed is generic enough to apply, to some extent, to other cases (J2EE platform, specific vendor JPA API…)
I Usage and use cases
@Transactional(value = "myTransactionManager", propagation = Propagation.REQUIRED) public void myMethod() { ... }
The value attribute of the @Transactional annotation is not mandatory. If not mentionned Spring will look by default for any bean declared in the context with the name “transactionManager” (defaultConvention).
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean>
II Registration in Spring context
<tx:annotation-driven/>
To make the @Transactional annotation work, you should declare the <tx:annotation-driven> tag (tx being the shortcut of the namespace for “http://www.springframework.org/schema/tx“)
III Code analysis
A Bean registration
In this chapter we will see how the <tx:annotation-driven> tag declaration is handled in the Spring context
1) org.springframework.transaction.config.AnnotationDrivenBeanDefinitionParser
/** * Parses the '<code></code>' tag. Will * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary register an AutoProxyCreator} * with the container as necessary. */ public BeanDefinition parse(Element element, ParserContext parserContext) { String mode = element.getAttribute("mode"); if ("aspectj".equals(mode)) { // mode="aspectj" registerTransactionAspect(element, parserContext); } else { // mode="proxy" // DEFAULT MODE AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); } return null; }
For most users, we fall into the else block (mode=”proxy”) so we’re calling AopAutoProxyConfigurer.configureAutoProxyCreator()
private static class AopAutoProxyConfigurer { public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); if (!parserContext.getRegistry().containsBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME)) { Object eleSource = parserContext.extractSource(element); // Create the TransactionAttributeSource definition. RootBeanDefinition sourceDef = new RootBeanDefinition(AnnotationTransactionAttributeSource.class); sourceDef.setSource(eleSource); sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // The bean AnnotationTransactionAttributeSource is created and registed dynamically here String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); // Create the TransactionInterceptor definition. // Point A RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); interceptorDef.setSource(eleSource); interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // Set the declared transaction manager in to the transactionInterceptor, when available registerTransactionManager(element, interceptorDef); interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); // The bean TransactionInterceptor is created and registed dynamically here String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); // Create the TransactionAttributeSourceAdvisor definition. // This bean is an AOP definition RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); advisorDef.setSource(eleSource); advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // Inject the bean AnnotationTransactionAttributeSource into the AOP definition // Point B advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); // Definition of advice bean = TransactionInterceptor previously declared advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); if (element.hasAttribute("order")) { advisorDef.getPropertyValues().add("order", element.getAttribute("order")); } // The bean BeanFactoryTransactionAttributeSourceAdvisor is created and registed dynamically here parserContext.getRegistry().registerBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME, advisorDef); CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, TRANSACTION_ADVISOR_BEAN_NAME)); parserContext.registerComponent(compositeDef); } } // Retrieve the transactionManager attribute defined on when available // Example private static void registerTransactionManager(Element element, BeanDefinition def) { def.getPropertyValues().add("transactionManagerBeanName", TxNamespaceHandler.getTransactionManagerName(element)); } }
The transaction interceptor class is created at line 19
Then the declared transaction manager in <tx:annotation-driven> is searched in the Spring context and attached to this transaction interceptor, line 24
Finally the bean is registered in the Spring context at line 28
From line 32 to line 47, Spring declares an TransactionAttributeSourceAdvisor bean and registers it into the context.
B @Transactional parsing
1) org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor
In this chapter, we’ll see how the @Transactional annotation is parsed during runtime by Spring to retrieve transaction-related properties
// Injected at Point B private TransactionAttributeSource transactionAttributeSource; // Define a pointcut here for AOP, injecting the transactionAttributeSource that was // set in AopAutoProxyConfigurer // see Point B private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { @Override protected TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } };
Here Spring just defines a poincut advisor. It is composed of a poincut handled by the class TransactionAttributeSourcePointcut. Here the transactionAttributeSource is passed to the anonymous class dynamically within the overriden getTransactionAttributeSource() method
2) abstract class org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut
abstract class org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut // Determine whether a call on a particular method matches the poincut // If it matches then the advice bean will be called // The advice bean that has been registered for this pointcut is the // TransactionInterceptor class (see Point A) public boolean matches(Method method, Class targetClass) { TransactionAttributeSource tas = getTransactionAttributeSource(); // Call getTransactionAttribute of the injected transactionAttributeSoirce // (see Point C) return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); }
This abstract class only defines the default bahavior of the maches() method to check whether the join point matches this poincut
3) org.springframework.transaction.annotation.AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource
// Should be false in a context of J2SE private static final boolean ejb3Present = ClassUtils.isPresent("javax.ejb.TransactionAttribute", AnnotationTransactionAttributeSource.class.getClassLoader()); // Default constructor public AnnotationTransactionAttributeSource() { this(true); } // publicMethodsOnly = true because this bean has been registered dynamically // by AopAutoProxyConfigurer with no argument so the default constructor above applies // // ejb3Present = false public AnnotationTransactionAttributeSource(boolean publicMethodsOnly) { this.publicMethodsOnly = publicMethodsOnly; this.annotationParsers = new LinkedHashSet(2); this.annotationParsers.add(new SpringTransactionAnnotationParser()); if (ejb3Present) { this.annotationParsers.add(new Ejb3TransactionAnnotationParser()); } }
The second constructor of AnnotationTransactionAttributeSource registers the SpringTransactionAnnotationParser as default parser for the @Transactional annotation
4) org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource
Point C & Point D
// Point C public TransactionAttribute getTransactionAttribute(Method method, Class targetClass) { // First, see if we have a cached value. Object cacheKey = getCacheKey(method, targetClass); Object cached = this.attributeCache.get(cacheKey); if (cached != null) { .... .... // Not interesting code } else { // We need to work it out. // (see Point D below) TransactionAttribute txAtt = computeTransactionAttribute(method, targetClass); // Put it in the cache. if (txAtt == null) { this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); } else { if (logger.isDebugEnabled()) { logger.debug("Adding transactional method '" + method.getName() + "' with attribute: " + txAtt); } this.attributeCache.put(cacheKey, txAtt); } return txAtt; } } // Point D private TransactionAttribute computeTransactionAttribute(Method method, Class targetClass) { // Don't allow no-public methods as required. // Here allowPublicMethodsOnly() will return true because we set the attribute // publicMethodOnly = true in the constructor of AnnotationTransactionAttribute if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } // Ignore CGLIB subclasses - introspect the actual user class. Class userClass = ClassUtils.getUserClass(targetClass); // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass); // If we are dealing with method with generic parameters, find the original method. specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); // Find the @Transactional attributes of the method in the target class. // (see Point E) TransactionAttribute txAtt = findTransactionAttribute(specificMethod); if (txAtt != null) { return txAtt; } ... // Not interesting code }
The getTransactionAttribute() of the abstract class ultimately delegates the work to the computeTransactionAttribute() method.
First it determines the target class (in case the annotation being put on an interface method) then calls the method findTransactionAttribute()
5) org.springframework.transaction.annotation.AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource
Point E
// Point E protected TransactionAttribute findTransactionAttribute(Method method) { // See below return determineTransactionAttribute(method); } protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) { for (TransactionAnnotationParser annotationParser : this.annotationParsers) { // (see Point F) TransactionAttribute attr = annotationParser.parseTransactionAnnotation(ae); if (attr != null) { return attr; } } return null; }
Again the real job is not done here but is delegated to the annotation parser class SpringTransactionAnnotationParser registered previously in the constructor of AnnotationTransactionAttributeSource
6) org.springframework.transaction.annotation.SpringTransactionAnnotationParser
Point F
// Point F public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { Transactional ann = ae.getAnnotation(Transactional.class); if (ann == null) { for (Annotation metaAnn : ae.getAnnotations()) { // @Transactional annotation ann = metaAnn.annotationType().getAnnotation(Transactional.class); if (ann != null) { break; } } } if (ann != null) { //See below return parseTransactionAnnotation(ann); } else { return null; } } public TransactionAttribute parseTransactionAnnotation(Transactional ann) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); rbta.setPropagationBehavior(ann.propagation().value()); rbta.setIsolationLevel(ann.isolation().value()); rbta.setTimeout(ann.timeout()); rbta.setReadOnly(ann.readOnly()); /* Set qualifier name in the case multiple transaction managers are used * bean id="myTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" * property name="entityManagerFactory" ref="myEntityManagerFactory" / * /bean * * @Transactional(value = "myTransactionManager") */ rbta.setQualifier(ann.value()); ArrayList rollBackRules = new ArrayList(); ... // Not interesting code return rbta; }
The parser will retrieve all attributes of the @Transactional annotation, among which:
- propagation behavior
- isolation level
- timeout value for the transaction
- readOnly flag
- and the most important attribute of all: value, which corresponds to the bean name of the transactionManager declared in the Spring context and responsible for the current transaction.
If omitted, the value attribute defaults to “transactionManager”. When dealing with multiple databases or multiple datasources applications, more than one transactionManager are defined in the Spring context so the value is important to help Spring choosing the right one.
C Transactional interceptor invocation
In this chapter we’ll look under the hood to see how Spring achieves transaction demarcations
1) org.springframework.transaction.interceptor.TransactionInterceptor extends TransactionAspectSupport
// The real job is done here public Object invoke(final MethodInvocation invocation) throws Throwable { // Work out the target class: may be <code>null</code>. // The TransactionAttributeSource should be passed the target class // as well as the method, which may be from an interface. Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // If the transaction attribute is null, the method is non-transactional. //Similar to Point C final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(invocation.getMethod(), targetClass); // (see Point G) final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(invocation.getMethod(), targetClass); // The txAttr is not null but the transactionManager is NOT an instance // of CallbackPreferringPlatformTransactionManager so we still enter the if block if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction() and commit/rollback calls. // (see Point H) TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceed(); } catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // Commit after the method call returns // (see Point O) commitTransactionAfterReturning(txInfo); return retVal; }
- First Spring retrieves the transaction attributes (line 12, 13 & 14)
- Then it gets the transaction manager from the Spring context and transaction attributes (line 17)
- A transaction is created by the underlying entity manager (line 26)
- The target method is invoked (line 33)
- After returning from the method invocation, the transaction is committed (line 45)
2) public abstract class org.springframework.transaction.interceptor.TransactionAspectSupport
Point G & Point H
//Point G protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) { if (this.transactionManager != null || this.beanFactory == null || txAttr == null) { return this.transactionManager; } String qualifier = txAttr.getQualifier(); // Case when the transaction manager has been declared directly in the @Transactional annotation // Example @Transactional(value = "myTransactionManager") if (StringUtils.hasLength(qualifier)) { return TransactionAspectUtils.getTransactionManager(this.beanFactory, qualifier); } // Case when the transaction manager has been declared in the tx:annotation-driven tag // Example tx:annotation driven transaction-manager="myTransactionManager" else if (this.transactionManagerBeanName != null) { return this.beanFactory.getBean(this.transactionManagerBeanName, PlatformTransactionManager.class); } ... // Not interesting code } // Point H protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) { // If no name specified, apply method identification as transaction name. // This is the default case if (txAttr != null && txAttr.getName() == null) { txAttr = new DelegatingTransactionAttribute(txAttr) { @Override public String getName() { return joinpointIdentification; } }; } TransactionStatus status = null; if (txAttr != null) { if (tm != null) { // Call to the AbstractPlatFormTransactionManager to start a transaction // (see Point I) status = tm.getTransaction(txAttr); } else { if (logger.isDebugEnabled()) { logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured"); } } } return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }
This class is doing 2 main tasks:
- determine the transaction manager to manage the current transaction, either using the value attribute of the @Transactional annotation or using the transaction-manager attribute of the the tx:annotation-driven tag
- delegates the creation of the transaction to the AbstractPlatFormTransactionManager class
3) abstract class org.springframework.transaction.support.AbstractPlatformTransactionManager
Point I
//Point I public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { // Retrieve the transaction from JpaTransactionManager.doGetTransaction() // (see Point J) Object transaction = doGetTransaction(); // Cache debug flag to avoid repeated checks. boolean debugEnabled = logger.isDebugEnabled(); if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); } ... // No existing transaction found -> check propagation behavior to find out how to proceed. if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } // Our case else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); /* * Return a new DefaultTransactionStatus(transaction, * newTransaction = true, * newSynchronization = true, * definition.isReadOnly(),debugEnabled, * suspendedResources) * for a new transaction */ DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); // Real job here, delegates call to JpaTransactionManager.doBegin() // (see Point K) doBegin(transaction, definition); // Set some synchronization flags to the TransactionSynchronizationManager thread local prepareSynchronization(status, definition); return status; } catch (RuntimeException ex) { resume(null, suspendedResources); throw ex; } catch (Error err) { resume(null, suspendedResources); throw err; } } else { // TransactionDefinition = PROPAGATION_SUPPORTS or PROPAGATION_NOT_SUPPORTED // or PROPAGATION_NEVER // Create "empty" transaction: no actual transaction, but potentially synchronization. boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); } }
The getTransaction() delegates the creation and the start of the transaction itself to the underlying JpaTransactionManager.
We can see here how Spring manages different types of Propagation behavior.
4) org.springframework.orm.jpa.JpaTransactionManager
Point J & Point K
//Point J protected Object doGetTransaction() { JpaTransactionObject txObject = new JpaTransactionObject(); txObject.setSavepointAllowed(isNestedTransactionAllowed()); // Try to retrieve an EntityManagerHolder from the thread local map of // TransactionSynchronizationManager using the EntityManagerFactory as search key // The EntityManagerFactory was injected in the JpaTransactionManager in the XML config file // // bean id="myTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" // property name="entityManagerFactory" ref="myEntityManagerFactory" // bean // // // this EntityManagerHolder might be null when called the first time EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(getEntityManagerFactory()); if (emHolder != null) { if (logger.isDebugEnabled()) { logger.debug("Found thread-bound EntityManager [" + emHolder.getEntityManager() + "] for JPA transaction"); } // attach the EntityManagerHolder to the JpaTransactionObject // the flag false is set to the property newEntityManagerHolder txObject.setEntityManagerHolder(emHolder, false); } // The datasource is injected directly into the JpaTransactionManager // after bean initialization (afterPropertySet()) // by inspecting the injected EntityManagerFactory // // bean id="myEntityManagerFactory" // class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" // property name="dataSource" ref="myDataSource" // property name="persistenceUnitName" value="myPersistenceUnit" // bean // // // this test always evaluates to true if (getDataSource() != null) { ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(getDataSource()); // attach a connectionHolder to the JpaTransactionObject (to start JDBC transaction probably) txObject.setConnectionHolder(conHolder); } return txObject; } //Point K protected void doBegin(Object transaction, TransactionDefinition definition) { JpaTransactionObject txObject = (JpaTransactionObject) transaction; ... try { // The EntityManagerHolder can be null if not registered already in the // thread local map of TransactionSynchronizationManager if (txObject.getEntityManagerHolder() == null || txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) { // Create a new EntityManager from the EntityManagerFactory EntityManager newEm = createEntityManagerForTransaction(); if (logger.isDebugEnabled()) { logger.debug("Opened new EntityManager [" + newEm + "] for JPA transaction"); } // attach the EntityManagerHolder to the JpaTransactionObject // newEntityManagerHolder = true // because the EntityManager has just been created from scratch txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true); } EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); final int timeoutToUse = determineTimeout(definition); /* Delegate to JpaDialect for actual transaction begin, passing the EntityManager * * META-INF|persistence.xml * * persistence-unit name="myPersistenceUnit" transaction-type="RESOURCE_LOCAL" * provider * org.hibernate.ejb.HibernatePersistence * provider * properties * property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" * ... */ // (see Point L for HibernateJpaDialect) Object transactionData = getJpaDialect().beginTransaction(em, new DelegatingTransactionDefinition(definition) { @Override public int getTimeout() { return timeoutToUse; } }); // Set transaction data to the JpaTransactionObject txObject.setTransactionData(transactionData); // Register transaction timeout. if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getEntityManagerHolder().setTimeoutInSeconds(timeoutToUse); } // Register the JPA EntityManager's JDBC Connection for the DataSource, if set. if (getDataSource() != null) { // Retrieve the underlying JDBC connection by calling the JPA Dialect class ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly()); if (conHandle != null) { ConnectionHolder conHolder = new ConnectionHolder(conHandle); if (timeoutToUse != TransactionDefinition.TIMEOUT_DEFAULT) { conHolder.setTimeoutInSeconds(timeoutToUse); } if (logger.isDebugEnabled()) { logger.debug("Exposing JPA transaction as JDBC transaction [" + conHolder.getConnectionHandle() + "]"); } // Set the JDBC connection to the current Threadlocal resources map, the // datasource being the key TransactionSynchronizationManager.bindResource(getDataSource(), conHolder); // Set JDBC connection holder to the JpaTransactionObject txObject.setConnectionHolder(conHolder); } else { if (logger.isDebugEnabled()) { logger.debug("Not exposing JPA transaction [" + em + "] as JDBC transaction because JpaDialect [" + getJpaDialect() + "] does not support JDBC Connection retrieval"); } } } // If the EntityManager has been created from scratch (see Point L) if (txObject.isNewEntityManagerHolder()) { // register the EntityManagerHolder to the current Threadlocal resources map, the EntityManagerFactory being the key TransactionSynchronizationManager.bindResource( getEntityManagerFactory(), txObject.getEntityManagerHolder()); } txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true); } catch (TransactionException ex) { closeEntityManagerAfterFailedBegin(txObject); throw ex; } catch (Exception ex) { closeEntityManagerAfterFailedBegin(txObject); throw new CannotCreateTransactionException("Could not open JPA EntityManager for transaction", ex); } }
Most of the important jobs are done in this class.
Point J : doGetTransaction()
-
first Spring tries to look in the TransactionSynchronizationManager ThreadLocal map to see if there is an existing entity manager using the entity manager factory as search key (lines 17 & 18)
-
The entity manager factory was injected into the Jpa transaction manager in the Spring XML definition.
If this is not done explicitely, Spring will do the job for you during initialization of the transaction manager by looking for a bean named “entityManagerFactory” (default name by convention) in the context.- If an entity manager is found in the ThreadLocal map, Spring wraps it around an EntityManagerHolder object with a boolean flag isNew = false since this entity manager has been created before hand somewhere in the code. (line 26)
- Otherwise the EntityManagerHolder of the JpaTransactionObject will be null
- Spring also retrieves the dataSource declared for this transaction manager and stores it in the JpaTransactionObject (line 46)
-
The entity manager factory was injected into the Jpa transaction manager in the Spring XML definition.
Point K : doBegin()
- Spring checks the JpaTransactionObject to look for an EntityManagerHolder.
- If not found, Spring delegates the creation of the entity manager to the attached entity manager factory (line 65). Then Spring wraps an EntityManagerHolder object around this entity manager with the flag isNew = true to indicate that this entity manager was created in the current transaction and not before (line 73)
- Then Spring delegates the creation of a new JDBC transaction to the underlying JPA Dialect (line 91 to 97). This dialect is defined in the META-INF/persistence.xml file for each persistenceUnit
- Spring registers the current JDBC connection to the TransactionSynchronizationManager ThreadLocal map using the dataSource as key (line 125)
- If the flag isNew = true is set on the JpaTransactionObject, Spring will also register the newly created entity manager to the TransactionSynchronizationManager ThreadLocal map using the entity manager factory as key.
5) org.springframework.orm.jpa.HibernateJpaDialect extends DefaultJpaDialect
Point L
// Point L public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { getSession(entityManager).getTransaction().setTimeout(definition.getTimeout()); } // (see Point M) super.beginTransaction(entityManager, definition); // (see Point N) return prepareTransaction(entityManager, definition.isReadOnly(), definition.getName()); }
Let’s consider the HibernateJpaDialect as default Jpa dialect. We can see that this class is calling the superclass DefaultJpaDialect to start the transaction (line 9)
Then it calls the internal method prepareTransaction() (line 12)
6) org.springframework.orm.jpa.DefaultJpaDialect
Point M
// Point M public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { throw new InvalidIsolationLevelException( "Standard JPA does not support custom isolation levels - " +"use a special JpaDialect for your JPA implementation"); } entityManager.getTransaction().begin(); return null; }
The transaction is started by the entity manager. We can clearly see that only the default ISOLATION level is supported by vanilla HibernateJpaDialect. Any attempt to set the isolation level to something other that ISOLATION_DEFAULT will trigger an Exception.
7) org.springframework.orm.jpa.HibernateJpaDialect extends DefaultJpaDialect
Point N
//Point N public Object prepareTransaction(EntityManager entityManager, boolean readOnly, String name) throws PersistenceException { Session session = getSession(entityManager); FlushMode flushMode = session.getFlushMode(); FlushMode previousFlushMode = null; if (readOnly) { // We should suppress flushing for a read-only transaction. session.setFlushMode(FlushMode.MANUAL); previousFlushMode = flushMode; } else { // We need AUTO or COMMIT for a non-read-only transaction. if (flushMode.lessThan(FlushMode.COMMIT)) { session.setFlushMode(FlushMode.AUTO); previousFlushMode = flushMode; } } return new SessionTransactionData(session, previousFlushMode); }
The prepareTransaction() method is setting and saving previous flush mode, nothing more that that…
8 ) org.springframework.transaction.interceptor.TransactionAspectSupport
Point O
// Point O protected void commitTransactionAfterReturning(TransactionInfo txInfo) { if (txInfo != null && txInfo.hasTransaction()) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); } // Delegate the commit call to the underlying TransactioManager // (see Point P) txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } }
This method is just delegating the transaction commit to the transaction manager
9) org.springframework.transaction.support.AbstractPlatformTransactionManager
Point P
// Point P public final void commit(TransactionStatus status) throws TransactionException { if (status.isCompleted()) { throw new IllegalTransactionStateException(Transaction is already completed - do not call commit or rollback more than once per transaction"); } ... // Not interesting code processCommit(defStatus); // See below } private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { // Commit pre-processing, not always implemented by the actual TransactionManager prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); … // Delegate the real commit to the actual TransactionManager // (see Point Q) doCommit(status); } ... try { // Commit post-processing, not always implemented by the actual TransactionManager triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { // (see Point M) cleanupAfterCompletion(status); } ... }
Again, apart from calling some trigger code to prepare the commit, the real job of committing is delegated to the method doCommit()
After the commit is done, cleanupAfterCompletion() is called to clean up the TransactionSynchronizationManager ThreadLocal map if necessary
10) org.springframework.orm.jpa.JpaTransactionManager
Point Q
//Point Q protected void doCommit(DefaultTransactionStatus status) { JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction(); if (status.isDebug()) { logger.debug("Committing JPA transaction on EntityManager [" + txObject.getEntityManagerHolder().getEntityManager() + "]"); } try { // The real commit is done here ! EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction(); tx.commit(); } catch (RollbackException ex) { if (ex.getCause() instanceof RuntimeException) { DataAccessException dex = getJpaDialect(). translateExceptionIfPossible((RuntimeException) ex.getCause()); if (dex != null) { throw dex; } } throw new TransactionSystemException("Could not commit JPA transaction", ex); } catch (RuntimeException ex) { // Assumably failed to flush changes to database. throw DataAccessUtils.translateIfNecessary(ex, getJpaDialect()); } }
Again, the commit is a plain call to getEntityManager().getTransaction().commit(), no magic in it.
11) org.springframework.transaction.support.AbstractPlatformTransactionManager
Point M
// Point M private void cleanupAfterCompletion(DefaultTransactionStatus status) { status.setCompleted(); if (status.isNewSynchronization()) { TransactionSynchronizationManager.clear(); } if (status.isNewTransaction()) { // (see Point N) doCleanupAfterCompletion(status.getTransaction()); } if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction after completion of inner transaction"); } resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources()); } }
Just an indirection of code, the real job is done in doCleanupAfterCompletion()
12) org.springframework.orm.jpa.JpaTransactionManager
Point N
// Point N protected void doCleanupAfterCompletion(Object transaction) { JpaTransactionObject txObject = (JpaTransactionObject) transaction; // Remove the entity manager holder from the thread. if (txObject.isNewEntityManagerHolder()) { TransactionSynchronizationManager.unbindResource(getEntityManagerFactory()); } txObject.getEntityManagerHolder().clear(); // Remove the JDBC connection holder from the thread, if exposed. if (txObject.hasConnectionHolder()) { TransactionSynchronizationManager.unbindResource(getDataSource()); try { getJpaDialect().releaseJdbcConnection( txObject.getConnectionHolder().getConnectionHandle(), txObject.getEntityManagerHolder().getEntityManager()); } catch (Exception ex) { // Just log it, to keep a transaction-related exception. logger.error("Could not close JDBC connection after transaction", ex); } } getJpaDialect().cleanupTransaction(txObject.getTransactionData()); // Remove the entity manager holder from the thread. if (txObject.isNewEntityManagerHolder()) { EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); if (logger.isDebugEnabled()) { logger.debug("Closing JPA EntityManager [" + em + "] after transaction"); } EntityManagerFactoryUtils.closeEntityManager(em); } else { logger.debug("Not closing pre-bound JPA EntityManager after transaction"); } }
Lots of interesting pieces of code here:
- Line 6 & 7 : if the JpaTransactionObject has its flag isNew = true then Spring remove its from the TransactionSynchronizationManager ThreadLocal map. Indeed isNew = true means that the entity manager was created from scratch for this current transaction and now since the transaction is committed there is no reason to keep it in the ThreadLocal map
- Similarly, at line 32 Spring will close gracefully the entity manager if flag isNew = true
- If the flag isNew = false meaning that the entity manager used in the current transaction has been registered in the TransactionSynchronizationManager ThreadLocal map before hand, nothing happens. It is not closed and still exists in the ThreadLocal map (line 35).
IV Summary
After digging into the Spring code for @Transactional, we can say that:
- There is many levels of indirection in the code. A single task like commit requires 3 method calls. I suppose it is due to the open & flexible architecture of Spring which allows end-users to plug their custom implementation of each component. It can also be explained by the fact that the transactional code should be as generic as possible so it can apply not only to JDBC transaction but also to other type of transactions (JMS, Web Services …)
- The TransactionSynchronizationManager plays the key role in the transaction management. It is acting as a thread-level cache to carry the current entity manager along all layers for the current transaction
- The TransactionSynchronizationManager public methods may suggest that it can be used programmatically to gain finer control on the lifecycle of the entity manager.
Pseudo-code for @Transactional management:
- Start a new DB transaction by calling the underlying JPADialect implementation getJpaDialect().beginTransaction(…)
- Commit the transaction by calling entityManager.getTransaction().commit()
- If the current entity manager was created from scratch, remove it from the ThreadLocal map and close it
- Else do nothing
Pingback: Hibernate Long/Extended session « Yet Another Java Blog
Pingback: Spring @PersistenceContext/@PersistenceUnit explained « Yet Another Java Blog
Pingback: JPA/Hibernate Global Conversation with Spring AOP « Yet Another Java Blog
Hi. I encounter this problem with TransactionAspectSupport http://stackoverflow.com/questions/15464232/spring-transactionaspectsupport-currenttransactionstatus-return-null. Could you please give me some clue on it 🙁
Thanks for the detailed analysis. It helps in understanding the spring transaction in a better way.