Why doesn't my (spring)HibernateTransactionManager work in wicket?
I tried to shorten this to what I think is relevant, I hope it's sufficient and not overwhelming. Please help!
I'm converting a small wicket+databinder+hibernate web application to use wicket+spring+hibernate. I have a DAO service class, with a hibernate SessionFactory injected by Spring. I am able to do read-only operations using the session factory (autocommit is on by default). What I want to do is use the HibernateTransactionManager and the @Transactional annotation to do a transactional operation.
I define a DAO service 开发者_如何学JAVAimplementation, which uses an injected SessionFactory in a method marked @Transactional:
public class DAO implements IDAO {
@SpringBean
private SessionFactory sessionFactory;
public DAO() {
super();
}
@Transactional
public Object execute(SessionUnit sessionUnit) {
Session sess = sessionFactory.getCurrentSession();
Object result;
result = sessionUnit.run(sess);
sess.flush();
return result;
}
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Transactional
public boolean isObjectPersistent(Object object) {
return sessionFactory.getCurrentSession().contains(object);
}
}
When I try to call isObjectPersistent()
, I get a hibernate exception because no one has called session.beginTransaction():
Caused by: org.hibernate.HibernateException: contains is not valid without active transaction
at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:338)
at $Proxy38.contains(Unknown Source)
at com.gorkwobbler.shadowrun.karma.db.hibernate.DAO.isObjectPersistent(DAO.java:35)
(reflection stuff omitted...)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
(reflection stuff omitted...)
at org.apache.wicket.proxy.LazyInitProxyFactory$JdkHandler.invoke(LazyInitProxyFactory.java:416)
at org.apache.wicket.proxy.$Proxy36.isObjectPersistent(Unknown Source)
I also notice from the full stack trace that the OpenSessionInViewFilter is being invoked, I'm not sure if that's relevant. Let me know if you need the rest of the stack trace.
If I create a custom WebRequestCycle subclass, which begins a transaction, I can get past this. This seems to me to undermine the purpose of @Transactional, and my implementation of it also turned out to be problematic.
Here is my applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Reference: http://wicketinaction.com/2009/06/wicketspringhibernate-configuration/ -->
<beans default-autowire="autodetect"
xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- bean definitions -->
<bean id="wicketApplication" class="com.gorkwobbler.shadowrun.karma.view.wicket.core.WicketApplication" />
<bean id="placeholderConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="ignoreUnresolvablePlaceholders" value="false" />
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
<property name="ignoreResourceNotFound" value="false" />
<property name="locations">
<list>
<value>classpath*:/application.properties</value>
</list>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName">
<value>${jdbc.driver}</value>
</property>
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="username">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
</bean>
<tx:annotation-driven transaction-manager="txManager" />
<!-- setup transaction manager -->
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
<!-- hibernate session factory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="configLocation">
<value>classpath:/hibernate.cfg.xml</value>
</property>
<property name="dataSource" ref="dataSource" />
<property name="hibernateProperties">
<props>
</props>
</property>
<property name="packagesToScan">
<list>
<value>com.gorkwobbler.shadowrun.karma.domain</value>
<value>com.gorkwobbler.shadowrun.karma.domain.*</value>
</list>
</property>
</bean>
<bean id="dao"
class="com.gorkwobbler.shadowrun.karma.db.hibernate.DAO">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
<!-- Don't know what this is for, but it was in the sample config I started from -->
<!-- <context:component-scan base-package="com.gorkwobbler.shadowrun.karma" /> -->
</beans>
How can I get my DAO to begin a transaction, commit at the end of that method, or rollback on error? I want to use the most minimal/standard configuration possible; I prefer annotations over XML if given the choice.
Edit:
I revised the applicationContext above to remove the AOP configuration stuff, which wasn't working, anyway.
Using the debugger, I determined that the SessionImpl stored in the TransactionInterceptor's session holder map is not the same session as the SessionImpl that is retrieved in the DAO method when I call sessionFactory.getCurrentSession(). Can anyone explain why this is? What am I doing wrong? The magic is not working. =(
Edit
I also notice the following message in my console during startup:
WARN - stractEhcacheRegionFactory - No TransactionManagerLookup found in Hibernate config, XA Caches will be participating in the two-phase commit!
It turns out that the problem was not actually in the configuration information that I posted. Sorry!
My above configuration links to an externalized hibernate.cfg.xml, which declared the following property:
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
I must've copied this from some sample hibernate config file somewhere. This property caused my SessionFactory to ignore the session context provided by spring, and use a thread-local context in its place. Removing this property fixed the problem (hibernate uses the JTA context by default if none is specified).
This can be made much simpler. Read this section 13.3.3 (Hibernate) Declarative transaction demarcation, especially the last part. This is usually enough configuration if you use @Transactional
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven/>
<bean id="myProductService" class="product.SimpleProductService">
<property name="productDao" ref="myProductDao"/>
</bean>
</beans>
To answer your comments:
No, if you inject the components via Spring, Spring does the wiring, hibernate doesn't need to know anything about spring, that's the whole point of using spring (to decouple the individual layers). Service and dao is a separation that not everybody uses. The important part is that the public methods backed by the interface are marked as transactional, because all methods marked as transactional will be intercepted by a proxy that does the transaction handling, while others won't.
You may want to read the general section about Declarative Transaction Managmenet in Spring to understand the process (this applies to all transactional technologies, not just to hibernate).
精彩评论