Spring LDAP Transaction – Unofficial yet Working Config Manual
之前的一篇文章介绍了Spring的LDAP子项目和ODM框架,其中提到了LDAP事务,但没有深入,而且那个配置中的事务也是不work的。上个周末在和JTA斗智斗勇的同时把项目中的LDAP事务也搞定了,现在可以做到将LDAP和Hibernate的session factory放在同一个事务上下文中进行ACID管理,即LDAP和数据库操作实现“all or none”(虽然是伪事务,具体下文会提到)。当然,对于Spring LDAP事务配置官方和Google上同样没有任何可参考或操作的文档说明,不然我也不用连着两个晚上码字造福大众了。另外,为了造福资本主义国家的程序猿们,同时向他们展示社会主义国家的制度优越性,以下将切换至英文
Spring LDAP is a amazing framework esp. for its LdapTemplate and ODM, providing a consistent point of view of developing LDAP code with the well-known and document-friendly techniques - JdbcTemplate and ORM. Unfortunately, Spring LDAP is not that widely-used, for its sluggish development progress (1.3.1 so far) and lacking of document/samples. This post will focus on a even rare yet important topic of Spring LDAP - transaction. For other information like O-D mapping or LDAP context source, pls refer to the official document, and some tips here if you can read Chinese.
Environment
- Spring LDAP 1.3.1
- Spring * 3.1.1
- Hibernate 4.1.1
Goal
- Implement a method annotated with @Transactional, which demonstrates a business service
- The service invokes two persist DAOs, one uses Hibernate's session factory and the other uses Spring's ODM
- The two persist actions should follow "all or none" rule, that is, if JDBC action fails after LDAP action, LDAP action should rollback, vice versa
IMPORTANT
As described in the official document, TX in Spring LDAP is "not real"
it should be noted that the provided support is all client side. The wrapped transaction is not an XA transaction. No two-phase as such commit is performed, as the LDAP server will be unable to vote on its outcome. Once again, however, for the majority of cases the supplied support will be sufficient.
Show time!
Spring configuration
<bean id="contextSourceTarget" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="url" value="..." /> <property name="base" value="dc=jayxu,dc=com" /> <property name="userDn" value="cn=admin,dc=jayxu,dc=com" /> <property name="password" value="..." /> </bean> <bean id="pooledContextSource" class="org.springframework.ldap.pool.factory.PoolingContextSource"> <property name="contextSource" ref="contextSourceTarget" /> <property name="testOnBorrow" value="true" /> <property name="dirContextValidator" ref="dirContextValidator" /> </bean> <bean id="dirContextValidator" class="org.springframework.ldap.pool.validation.DefaultDirContextValidator" /> <bean class="org.springframework.ldap.core.LdapTemplate"> <property name="contextSource" ref="contextSource" /> </bean> <bean id="contextSource" class="org.springframework.ldap.transaction.compensating.manager.TransactionAwareContextSourceProxy"> <constructor -arg ref="pooledContextSource" /> </bean> <bean id="ldapTransactionManager" class="com.jayxu.common.ldap.ContextSourceAndHibernate4TransactionManager"> <property name="contextSource" ref="contextSource" /> <property name="sessionFactory" ref="sessionFactory" /> </bean> <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImplFactoryBean"> <property name="contextSource" ref="contextSource" /> <property name="managedClasses"> <set> <value>...</value> </set> </property> ... </bean> <bean class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager" ref="ldapTransactionManager" /> <property name="target" ref="myService" /> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRES_NEW</prop> </props> </property> </bean> <bean id="myService" class="com.jayxu.service.MyService" /> <tx:annotation-driven />
This diagram gives a overview of the beans' references
In your service class, you should annotate your transactional methods or the whole class with
@Transactional(value = "ldapTransactionManager")
here "value" refers to the tx manager name above. My test code looks like
@Transactional(value = "ldapTransactionManager") public void ldapTx() { odm.create(...); sessionFactory.getCurrentSession().persist(...); throw new RuntimeException(); }
then config logger level of org.springframework.ldap and org.springframework.transaction to DEBUG, if everything works well, you can find something in the output like
[raw] [DEBUG] org.springframework.ldap.transaction.compensating.LdapCompensatingTransactionOperationFactory:59 - Bind operation recorded [DEBUG] org.springframework.ldap.transaction.compensating.BindOperationExecutor:97 - Performing bind operation [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:140 - Retrieved value [org.springframework.ldap.transaction.compensating.manager.DirContextHolder@3ed024df] for key [org.springframework.ldap.pool.factory.PoolingContextSource@165b3858] bound to thread [main] [DEBUG] org.springframework.ldap.transaction.compensating.manager.TransactionAwareDirContextInvocationHandler:120 - Leaving transactional context open [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:140 - Retrieved value [org.springframework.orm.hibernate4.SessionHolder@55661f7b] for key [org.hibernate.internal.SessionFactoryImpl@7fb5438d] bound to thread [main] [DEBUG] org.springframework.transaction.interceptor.TransactionInterceptor:406 - Completing transaction for [com.jayxu.service.UserLdapService.addUserTx] after exception: java.lang.RuntimeException [DEBUG] org.springframework.transaction.interceptor.RuleBasedTransactionAttribute:130 - Applying rules to determine whether transaction should rollback on java.lang.RuntimeException [DEBUG] org.springframework.transaction.interceptor.RuleBasedTransactionAttribute:147 - Winning rollback rule is: null [DEBUG] org.springframework.transaction.interceptor.RuleBasedTransactionAttribute:152 - No relevant rollback rule found: applying default rules [DEBUG] org.springframework.transaction.compensating.support.DefaultCompensatingTransactionOperationManager:79 - Performing rollback [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:331 - Clearing transaction synchronization [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:243 - Removed value [org.springframework.orm.hibernate4.SessionHolder@55661f7b] for key [org.hibernate.internal.SessionFactoryImpl@7fb5438d] from thread [main] [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:243 - Removed value [org.springframework.jdbc.datasource.ConnectionHolder@22013e9b] for key [org.springframework.jdbc.datasource.DriverManagerDataSource@c20e54a] from thread [main] [DEBUG] org.springframework.transaction.compensating.support.AbstractCompensatingTransactionManagerDelegate:121 - Cleaning stored transaction synchronization [DEBUG] org.springframework.transaction.support.TransactionSynchronizationManager:243 - Removed value [org.springframework.ldap.transaction.compensating.manager.DirContextHolder@3ed024df] for key [org.springframework.ldap.pool.factory.PoolingContextSource@165b3858] from thread [main] [DEBUG] org.springframework.ldap.transaction.compensating.manager.ContextSourceTransactionManagerDelegate:103 - Closing target context [/raw]
Line 1 indicates save point being created while line 10 indicates the rollback