最近将手头的项目由 Hibernate 3.6.10.Final 升级到 Hibernate 5.0.12.Final。运行了一段时间以后发现异常,日志内容如下
org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.
at org.springframework.orm.hibernate5.HibernateTemplate.checkWriteOperationAllowed(HibernateTemplate.java:1165)
at org.springframework.orm.hibernate5.HibernateTemplate$14.doInHibernate(HibernateTemplate.java:670)
at org.springframework.orm.hibernate5.HibernateTemplate.doExecute(HibernateTemplate.java:359)
at org.springframework.orm.hibernate5.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:326)
at org.springframework.orm.hibernate5.HibernateTemplate.update(HibernateTemplate.java:667)
at org.springframework.orm.hibernate5.HibernateTemplate.update(HibernateTemplate.java:662)
可以确定的是,相关部分的业务代码没有做过改动,那么问题一定是出在框架升级上。查看 Hibernate 源码,发现了问题的直接原因。
// org.springframework.orm.hibernate5.HibernateTemplate
protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNativeSession) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Session session = null;
boolean isNew = false;
try {
session = getSessionFactory().getCurrentSession();
}
catch (HibernateException ex) {
logger.debug("Could not retrieve pre-bound Hibernate session", ex);
}
if (session == null) {
session = getSessionFactory().openSession();
session.setFlushMode(FlushMode.MANUAL);
isNew = true;
}
...
}
可以看到,当获取不到 Session 时,Hibernate 会新建一个 session,然后设置为只读模式(FlushMode.MANUAL)。那么为什么 session 为空呢?回头看调用的地方
public class FooServiceImpl implements FooService {
public void foo () {
FooThread fooThread = new FooThread();
// ...
}
private class FooThread extends Thread {
public void run() { FooServiceImpl.this.update(someData); }
}
}
fooThread 中的 fooService 对象,实际上是 FooServiceImpl.this,也就是未经 Spring 代理的对象。而我们的事务仅配置在 service 层,并没有配置在 dao 层,所以在上下文中找不到 session。
既然如此,为什么 Hibernate 3 中没有出现同样的异常呢?同样查看 Hibernate 3 的源码,发现 Spring 针对 Hibernate 3 和 5 的实现有比较大的差别
// org.springframework.orm.hibernate3.HibernateTemplate
protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNewSession, boolean enforceNativeSession)
throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Session session = (enforceNewSession ?
SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession());
...
}
创建 session 后,并没有手动修改 FlushMode 的操作。继续深入,发现和 针对 Hibernate 5 的实现一样,创建 session 的方法实际上是
SessionFactory.openSession(),该方法创建的 session 默认是 FlushMode.AUTO 的,与 5 不同的是没有强制修改的操作。而该 flushMode 不会触发上面的异常。
找到了问题的根本原因,解决起来就非常容易了,只需要将该对象替换为被 Spring 事务代理的对象即可。
总结
- Spring + Hibernate 5 对事务要求更严格,所有读写操作必须通过事务提交
- 避免使用
this
在 service 层创建对象。同理,所有需要 Spring AOP 的操作都要尽量避免在类内部调用,或者使用 AspectJ 实现。 - 在 dao 层配置事务也是不错的选择
完