浅淡Write operations not allowed异常及Spring编程式事务管理

一、异常引出

  下面这个异常,使用Spring + Hibernate的很多人应该碰到过:
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.
    一般是Dao的某个方法对数据库进行写操作时,抛出该异常。
    
  根据提示信息,两种解决思路:
  1、因为当前的FlushMode为MANUAL,该模式只能读,需要更改为FlushMode.COMMIT或FlushMode.AUTO
  2、去除事务定义里的readOnly属性(设置为false)

二、声明式事务管理的解决方法

  先说一下解决方法。假设抛异常的方法叫saveUser(),
  如果是声明式事务管理,有两种解决方法:

  1、针对注解方式

    @Transactional(readOnly = false)
        public void saveUser(User user)
    

  2、针对AOP xml方式

         <tx:advice id="serviceAdvice">
            <tx:attributes>
                <tx:method name="save*" read-only="false" propagation="REQUIRED"/>
            </tx:attributes>
        </tx:advice>
        
        <aop:config>
            <aop:pointcut id="servicePointCut" expression="execution(* com.example.service.*ServiceImp.*(..))"/>
            <aop:advisor advice-ref="serviceAdvice" pointcut-ref="servicePointCut"/>
        </aop:config>

    对于编程式事务管理,其解决方法就涉及到了编程式事务管理的具体实现。
    

三、基础编程式事务管理

        先看一个最基础的例子,后面再解释。
    
        applicationContext.xml中,仅配置SessionFactory。如下: 
        <!--DataSource-->
        <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
            <property name="driverClassName" value="${dataSource.driverName}"/>
            <property name="username" value="${dataSource.username}"/>
            <property name="url" value="${dataSource.url}"/>
            <property name="password" value="${dataSource.password}"/>
        </bean>

        <!--SessionFactory-->
        <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
            <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
        </bean>     
        
        Java代码:      
        SessionFactory sessionFactory = (SessionFactory) applicationContext.getBean("sessionFactory");
        //HibernateTemplate也可以在applicationContext.xml中配置,随意发挥
        HibernateTemplate hibernateTemplate = new HibernateTemplate(sessionFactory);

        //自定义事务属性、规则
        //这里想在applicationContext.xml中配置bean也可以,随意发挥
        TransactionDefinition transactionDefinition = new TransactionDefinition()
        {
            public int getPropagationBehavior()
            {
                return PROPAGATION_REQUIRED;
            }

            public int getIsolationLevel()
            {
                return ISOLATION_REPEATABLE_READ;
            }

            public int getTimeout()
            {
                return TIMEOUT_DEFAULT;
            }

            public boolean isReadOnly()
            {
                //设置readOnly为false
                //解决文章开头提到的异常
                return false;
            }

            public String getName()
            {
                return "thomas-transaction";
            }
        };

        //因为使用的是Hibernate,所以就使用HibernateTransactionManager
        //也可以使用其他的,如DataSourceTransactionManager(针对JDBC)
        PlatformTransactionManager txManager = new HibernateTransactionManager(sessionFactory);
        //开始事务
        TransactionStatus txStatus = txManager.getTransaction(transactionDefinition);
        try{
            hibernateTemplate.save(new User());
            //提交事务
            txManager.commit(txStatus);
        }catch (Exception e)
        {
            e.printStackTrace();
            //事务回滚
            txManager.rollback(txStatus);
        }     

四、3个底层接口

    上述代码示例中,可看到事务管理的部分,基本上是三个接口在起作用,即TransactionDefinition、
    PlatformTransactionManager和TransactionStatus。
    
    那么这三个接口的作用是什么?

    1、TransactionDefinition

        该接口用于定义事务的属性和规则,即需要产生的事务应该符合哪些规则。   
        public interface TransactionDefinition {
            int getPropagationBehavior();
            int getIsolationLevel();
            int getTimeout();
            boolean isReadOnly();
            String getName();
        }  

        1.1、isolationLevel————隔离级别

            事务的四个特性ACID,该属性即对应I,隔离级别。可选值:
            (1)TransactionDefinition.ISOLATION_DEFAULT
                默认值,表示使用底层数据库的默认隔离级别。
            (2)TransactionDefinition.ISOLATION_READ_UNCOMMITTED
                可以读取另一个事务修改但还没有提交的数据。
            (3)TransactionDefinition.ISOLATION_READ_COMMITTED
                只能读取另一个事务已经提交的数据。
            (4)TransactionDefinition.ISOLATION_REPEATABLE_READ
                可重复读。MySQL的默认隔离级别
            (5)TransactionDefinition.ISOLATION_SERIALIZABLE
                串行。所有的事务依次逐个执行,完全不可能产生干扰,会严重影响程序的性能。 

        1.2、propagationBehavior————传播行为

            所谓事务的传播行为是指,如果在开始当前事务之前,
            一个事务上下文已经存在,此时有若干选项可以指定一个
            事务性方法的执行行为。可选值:
 
            (1)TransactionDefinition.PROPAGATION_REQUIRED
                如果当前存在事务,则加入该事务。如果没有,则创建一个新的事务
            (2)TransactionDefinition.PROPAGATION_REQUIRES_NEW
                创建一个新的事务,如果当前存在事务,则把当前事务挂起
            (3)TransactionDefinition.PROPAGATION_SUPPORTS
                如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行。
            (4)TransactionDefinition.PROPAGATION_NOT_SUPPORTED
                以非事务方式运行,如果当前存在事务,则挂起。
            (5)TransactionDefinition.PROPAGATION_NEVER
                以非事务方式运行,如果当前存在事务,则抛出异常。
            (6)TransactionDefinition.PROPAGATION_MANDATORY
                如果当前存在事务,则加入。否则抛出异常。
            (7)TransactionDefinition.PROPAGATION_NESTED
                如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。
                若没有事务,则等价于TransactionDefinition.PROPAGATION_REQUIRED

        1.3、timeout————超时时间

            若事务运行超出设定时间,则自动回滚。
            可设置为TIMEOUT_DEFAULT,表示采用底层数据库事务机制的默认超时时间。

        1.4、readOnly————只读

            事务是否只能读数据而不能写数据。
            文章开头提到的异常,其重点就在这里了。        

        1.5、name————名字

            为事务起一个可读的、描述性的名字      

    2、PlatformTransactionManager

        PlatformTransactionManager接口屏蔽各个底层事务设施的差异,
        根据TransactionDefinition接口的规则,在事务栈中寻找符合规则的事务,
        并开启之。如果没找到符合规则的,则创建一个新的事务。
        该接口还提供事务提交和回滚等管理操作。
        
        其定义如下: 
        public interface PlatformTransactionManager
        {
            TransactionStatus getTransaction
                (TransactionDefinition definition) throws TransactionException;
            
            void commit(TransactionStatus status) throws TransactionException;
        
            void rollback(TransactionStatus status) throws TransactionException;
        }

        默认情况下,对于RuntimeException会回滚,而对于CheckedException不会回滚。 

    3、TransactionStatus

        该接口表征一个事务的状态。如:事务是否已完成(提交或回滚),
        该事务是否是新建的、是否有保存点等。     
        public interface TransactionStatus extends SavepointManager {
            boolean isNewTransaction();
            boolean hasSavepoint();
            void setRollbackOnly();
            boolean isRollbackOnly();
            void flush();
            boolean isCompleted();
        }   

五、进阶编程式事务管理

    上面的 三、基础编程式事务管理 所给出的例子真的是基础示例,存在着可以改进的地方。
    比如定义一个TransactionDefinition时,一般不会自己去new一个匿名对象。
    
    Spring提供了默认的TransactionDefinition接口的实现类————DefaultTransactionDefinition,
    完全可以直接使用这个类的实例。或者通过Spring的IOC来注入而不是手动new,随意发挥嘛。
    
    Spring提供了一个TransactionTemplate辅助类,便于编程式事务管理。
    该类的核心方法是public <T> T execute(TransactionCallback<T> action)。
    事务操作,事务管理都在这个方法里完成。
    
    TransactionTemplate继承了DefaultTransactionDefinition,因此可以通过
    TransactionTemplate实例来修改事务规则。
    
    示例如下:
    在applicationContext.xml中配置TransactionManager
   
    <!--DataSource-->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${dataSource.driverName}"/>
        <property name="username" value="${dataSource.username}"/>
        <property name="url" value="${dataSource.url}"/>
        <property name="password" value="${dataSource.password}"/>
    </bean>

    <!--SessionFactory-->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
    </bean>

    <!--Transaction Manager-->
    <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>  
  
    Java代码: 
    HibernateTransactionManager transactionManager =
                (HibernateTransactionManager) applicationContext.getBean("transactionManager");

    final HibernateTemplate hibernateTemplate = new HibernateTemplate(transactionManager.getSessionFactory());

    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    //解决文章开头异常的关键
    transactionTemplate.setReadOnly(false);
    Serializable id = transactionTemplate.execute(new TransactionCallback<Serializable>()
    {
        public Serializable doInTransaction(TransactionStatus status)
        {
            Serializable id = null;
            try{
                User user = new User(username, password, UserType.CUSTOMER, email, null);
                id = hibernateTemplate.save(user);
            }catch (Exception e)
            {
                e.printStackTrace();
                status.setRollbackOnly();
            }
            return id;
        }
    });
    System.out.println(id);    

六、TransactionTemplate与HibernateTemplate

    TransactionTemplate有一个execute(TransactionCallback<T> callback)方法用于事务管理。
    而HibernateTemplate也有一个类似的execute(new HibernateCallback<T>() callback)方法。  
    那一个自然的想法就是,能不能在HibernateTemplate的execute()方法中进行事务管理?
    
    Spring官方的文档是这样说的:
    Note: Callback code is not supposed to handle transactions itself!
    Use an appropriate transaction manager like HibernateTransactionManager.
    Generally, callback code must not touch any Session lifecycle methods,
    like close, disconnect, or reconnect, to let the template do its work.
    
    所以一目了然了,HibernateTemplate管事务具体的操作(增删改查),
    TransactionTemplate负责事务的管理(开启、提交、回滚、结束)。  

七、flushMode

    回到文章最初的异常,提示信息中提到设置FlushMode。
    熟悉Hibernate的都知道这是指Hibernate的Session类的setFlushMode()方法。
    这又是从何而来?
    
    异常是由于HibernateTemplate的save或update等方法,进行写操作而引发的。
    这里分两种情况:
    1、不使用事务管理器
    2、使用事务管理器,但readOnly = true
    
    看一下HibernateTemplate中save()的源码:  
    @Override
    public Serializable save(final Object entity) throws DataAccessException {
        return executeWithNativeSession(new HibernateCallback<Serializable>() {
            @Override
            public Serializable doInHibernate(Session session) throws HibernateException {
                checkWriteOperationAllowed(session);
                return session.save(entity);
            }
        });
    }
    
    追踪executeWithNativeSession方法:
    public <T> T executeWithNativeSession(HibernateCallback<T> action) {
        return doExecute(action, true);
    }
    
    追踪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,则不会有随TransactionManager
            //的初始化而创建的Session实例
            session = getSessionFactory().getCurrentSession();
        }
        catch (HibernateException ex) {
            logger.debug("Could not retrieve pre-bound Hibernate session", ex);
        }

        //此处session为null,Spring调用Hibernate的API新建一个
        //这里是关键之一,新建的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);
            }
        }
    }
    
    回到一开始的HibernateTemplate的save()方法:
    @Override
    public Serializable save(final Object entity) throws DataAccessException {
        return executeWithNativeSession(new HibernateCallback<Serializable>() {
            @Override
            public Serializable doInHibernate(Session session) throws HibernateException {
                checkWriteOperationAllowed(session);
                return session.save(entity);
            }
        });
    }
    
    追踪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是否小于FlushMode.COMMIT。
    如果小于,则抛异常。可看到,异常的信息正是文章开头出现的异常信息。
    FlushMode的枚举值如下:    
    @Deprecated
    NEVER ( 0 ),
    MANUAL( 0 ),
    COMMIT(5 ),
    AUTO(10 ),
    ALWAYS(20 );
    
    因此,如果没有使用TransactionManager,则Spring会自动创建一个Session用来执行数据库操作。
    但这个默认的Session的flushmode是FlushMode.MANUAL,须手动flush。
    也就是说,这个情况下,基本上只能手动使用Hibernate的API了。
    通过hibernateTemplate.getSessionFactory().getCurrentSession()可得到Session对象,
    再手动调用Session的setFlushMode()方法,可解决文章开头的异常。  
    
    那第2种情况呢?如果使用了TransactionManager,但readOnly = true的情况呢?
    这种情况下,在实例化HibernateTransactionManager时(此时还没执行hibernateTemplate.save()),
    其doBegin()方法有如下几行代码:  
    if (definition.isReadOnly() && txObject.isNewSession()) {
        // Just set to MANUAL in case of a new Session for this transaction.
        session.setFlushMode(FlushMode.MANUAL);
    }

    if (!definition.isReadOnly() && !txObject.isNewSession()) {
        // We need AUTO or COMMIT for a non-read-only transaction.
        FlushMode flushMode = session.getFlushMode();
        if (session.getFlushMode().equals(FlushMode.MANUAL)) {
            session.setFlushMode(FlushMode.AUTO);
            txObject.getSessionHolder().setPreviousFlushMode(flushMode);
        }
    }
    可以看到,随着HibernateTransactionManager的初始化,会产生一个Session实例,
    但如果readOnly = true,则同样地,该Session的flushmode被设置为FlushMode.MANUAL。
    
    这就是文章开头所出现的异常根本原因。

你可能感兴趣的:(spring,Hibernate)