美文网首页
Spring 配置 FAQ

Spring 配置 FAQ

作者: zephyrous | 来源:发表于2018-01-12 13:33 被阅读0次

    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

    相关文章

      网友评论

          本文标题:Spring 配置 FAQ

          本文链接:https://www.haomeiwen.com/subject/ujhfoxtx.html