Spring 配置 FAQ

2018-01-12  本文已影响0人  zephyrous

Spring 配置 FAQ

无法保存对象

错误提示: 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 的 save() 方法中的 checkWriteOperationAllowed() 方法:

protected void checkWriteOperationAllowed(Session session) throws InvalidDataAccessApiUsageException {
    if (isCheckWriteOperations() && session.getFlushMode().lessThan(FlushMode.COMMIT)) {
            throw new 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.");
        }
    }

表面上看是因为 Session 设置了 FlushMode.MANUAL 或者事务定义了 readOnly = true,导致该对象不能写入数据库。但实际上是因为该对象在没有事务管理的情况下进行写入,才导致无法保存,出现上述错误。

详细原因

查看 HibernateTemplate 中 save() 的源码,发现其调用顺序为: save() --> executeWithNativeSession() --> doExecute()。查看 doExecute() 源码如下:

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 {
            /* 如果没有使用 TransactionManager(包括 HibernateTransactionManager),
             * 则 session 的值仍然为 null。*/
            session = getSessionFactory().getCurrentSession();
        }
        catch (HibernateException ex) {
            logger.debug("Could not retrieve pre-bound Hibernate session", ex);
        }

        /* 有上述可知 session == null,因此,在此处 Spring 调用 Hibernate 的 API 新建一个 session。 
         * 新建的 Session 的 FlushMode 是 MANUAL,只读不可写。
         */
        if (session == null) {
            session = getSessionFactory().openSession();
            session.setFlushMode(FlushMode.MANUAL);
            isNew = true;
        }

        try {
            enableFilters(session);
            Session sessionToExpose =
                    (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session));
            /* 如果开启了事务,则跳过 if(session == null) 执行到这里
             * doInHibernate 在 save() 方法中,以匿名内部类的方式定义。
             */
            return action.doInHibernate(sessionToExpose);
        }
        catch (HibernateException ex) {
            throw SessionFactoryUtils.convertHibernateAccessException(ex);
        }
        catch (RuntimeException ex) {
            // Callback code threw application exception...
            throw ex;
        }
        finally {
            if (isNew) {
                SessionFactoryUtils.closeSession(session);
            }
            else {
                disableFilters(session);
            }
        }
    }

解决方案

首先,要配置 Spring 的事务管理。然后,必须用 Spring 中的 Bean 来注入到具体实现类中。

在 Spring 配置文件中设置事务管理

<!-- 配置 Hibernate 事务管理 -->
    <bean id = "hibernateTransactionManager"
          class = "org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name = "sessionFactory" ref = "mysqlSessionFactory" />
    </bean>

然后有两种方式让 sava() 方法在事务管理中进行:

1. 在 Spring 配置文件中设置 TransactionProxyFactoryBean
<!-- 配置 事务代理工厂Bean -->
    <bean id = "hibernateTransactionProxyFactoryBean"
          class = "org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name = "transactionManager" ref = "hibernateTransactionManager" />
        <property name = "transactionAttributeSource">
            <props>
                <prop key = "*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
        <property name = "target">
            <bean class = "XXX.dao.impl.BaseDaoHibernateImpl">
                <property name = "sessionFactory" ref = "mysqlSessionFactory" />
            </bean>
        </property>
    </bean>

注意!

<property name = "transactionAttributes">

<property name = "transactionAttributeSource">

只能二选一。建议使用 transactionAttributes,因为这样配置起来更简洁、方便。另外,

<property name = "target">`

必须设置,否则会报错。其目的是为需要使用事务的类声明匿名类。需要把所有用到事务的类都写进去。其他属性详见 Spring 文档。

2. 使用 Spring 注解

在 Spring 配置文件中添加

<tx:annotation-driven transaction-manager = "hibernateTransactionManager" />

其作用是开启事务注解。然后在 DAO 层的具体实现类或方法的上一行添加

@Transactional (rollbackFor = Exception.class)

即可。例如:

@Transactional (rollbackFor = Exception.class)
public class BaseDaoHibernateImpl<T> extends HibernateDaoSupport implements BaseDAO<T> {
...
}

添加具体实现类的 Bean

在 Spring 中添加 DAO 层的具体实现类,例如:

<bean id="dao" class = "XXX.impl.BaseDaoHibernateImpl">
    <property name = "sessionFactory" ref = "mysqlSessionFactory" />
</bean>

然后在 Java 代码中获取这个 bean。具体获取可以按照自己的项目来做。这里给个参考例子:

(BaseDAO) applicationContext.getBean ("dao");

结语

Spring 管理事务是非常好用的。但是所有和事务有关的 bean 必须用 Spring 来声明,不能用 new 来声明。否则 new 出来的对象用不了 Spring 中的各种配置。

参考文献

  1. 浅淡Write operations not allowed异常及Spring编程式事务管理, http://www.voidcn.com/article/p-ajtcxbco-beu.html
上一篇 下一篇

猜你喜欢

热点阅读