zoukankan      html  css  js  c++  java
  • spring的事务

    事务必需满足ACID(原子性、一致性、隔离性和持久性)特性,缺一不可:

    • 原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做
    • 一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态。
    • 隔离性(Isolation):并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;
    • 持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失

    Java EE事务类型有本地事务和全局事务

    • JDBC事务:就是数据库事务类型中的本地事务,通过Connection对象的控制来管理事务
    • 本地事务:使用JDBC编程实现事务;

    Spring提供的事务管理

    Spring框架最核心功能之一就是事务管理,而且提供一致的事务管理抽象,这能帮助我们:

    • 提供一致的编程式事务管理API,不管使用Spring JDBC框架还是集成第三方框架使用该API进行事务编程
    • 无侵入式的声明式事务支持

    Spring框架支持事务管理的核心是事务管理器抽象,对于不同的数据访问框架(如Hibernate)通过实现策略接口PlatformTransactionManager,从而能支持各种数据访问框架的事务管理

    Java代码  收藏代码
    1. public interface PlatformTransactionManager {  
    2.        TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;  
    3.        void commit(TransactionStatus status) throws TransactionException;  
    4.        void rollback(TransactionStatus status) throws TransactionException;  
    5. }  
    • getTransaction():返回一个已经激活的事务或创建一个新的事务(根据给定的TransactionDefinition类型参数定义的事务属性),返回的是TransactionStatus对象代表了当前事务的状态,其中该方法抛出TransactionException(未检查异常)表示事务由于某种原因失败。
    • commit()用于提交TransactionStatus参数代表的事务
    • rollback()用于回滚TransactionStatus参数代表的事务

    TransactionDefinition接口定义如下:

    Java代码  收藏代码
    1. public interface TransactionDefinition {  
    2.        int getPropagationBehavior();  
    3.        int getIsolationLevel();  
    4.        int getTimeout();  
    5.        boolean isReadOnly();  
    6.        String getName();  
    7. }  
    • getPropagationBehavior()返回定义的事务传播行为;
    • getIsolationLevel()返回定义的事务隔离级别;
    • getTimeout()返回定义的事务超时时间;
    • isReadOnly()返回定义的事务是否是只读的;
    • getName()返回定义的事务名字。

    TransactionStatus接口定义如下:

    Java代码  收藏代码
    1. public interface TransactionStatus extends SavepointManager {  
    2.        boolean isNewTransaction();  
    3.        boolean hasSavepoint();  
    4.        void setRollbackOnly();  
    5.        boolean isRollbackOnly();  
    6.        void flush();  
    7.        boolean isCompleted();  
    8. }  
    • isNewTransaction():返回当前事务状态是否是新事务
    • hasSavepoint():返回当前事务是否有保存点
    • setRollbackOnly()设置当前事务应该回滚;
    • isRollbackOnly(()返回当前事务是否应该回滚;
    • flush()用于刷新底层会话中的修改到数据库,一般用于刷新如Hibernate/JPA的会话,可能对如JDBC类型的事务无任何影响;
    • isCompleted():当前事务否已经完成。

    内置事务管理器实现

    Spring提供了许多内置事务管理器实现:

    • DataSourceTransactionManager位于org.springframework.jdbc.datasource包中,数据源事务管理器,提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理;
    • HibernateTransactionManager位于org.springframework.orm.hibernate3包中,提供对单个org.hibernate.SessionFactory事务支持,用于集成Hibernate框架时的事务管理;该事务管理器只支持Hibernate3+版本,且Spring3.0+版本只支持Hibernate 3.2+版本;

      等等。。。。。。

    声明对本地事务的支持

    a)JDBC及iBATIS、MyBatis框架事务管理器

    Java代码  收藏代码
    1. <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    2.     <property name="dataSource" ref="dataSource"/>  
    3. </bean>  

    通过dataSource属性指定需要事务管理的单个javax.sql.DataSource对象。

    d)Hibernate事务管理器

    Java代码  收藏代码
    1. <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
    2.     <property name="sessionFactory" ref="sessionFactory"/>  
    3. </bean>  

    通过entityManagerFactory属性指定需要事务管理的org.hibernate.SessionFactory对象。

    编程式事务概述

           所谓编程式事务指的是通过编码方式实现事务,即类似于JDBC编程实现事务管理。

           Spring框架提供一致的事务抽象,因此对于JDBC还是JTA事务都是采用相同的API进行编程。

    Java代码  收藏代码
    1. Connection conn = null;  
    2. UserTransaction tx = null;  
    3. try {  
    4.     tx = getUserTransaction();                       //1.获取事务  
    5.     tx.begin();                                    //2.开启JTA事务  
    6.     conn = getDataSource().getConnection();           //3.获取JDBC  
    7.     //4.声明SQL  
    8.     String sql = "select * from INFORMATION_SCHEMA.SYSTEM_TABLES";  
    9.     PreparedStatement pstmt = conn.prepareStatement(sql);//5.预编译SQL  
    10.     ResultSet rs = pstmt.executeQuery();               //6.执行SQL  
    11.     process(rs);                                   //7.处理结果集  
    12.     closeResultSet(rs);                             //8.释放结果集  
    13.     tx.commit();                                  //7.提交事务  
    14. catch (Exception e) {  
    15.     tx.rollback();                                 //8.回滚事务  
    16.     throw e;  
    17. finally {  
    18.    conn.close();                                //关闭连接  
    19. }  

           此处可以看到使用UserTransaction而不是Connection连接进行控制事务,从而对于JDBC事务和JTA事务是采用不同API进行编程控制的,并且JTA和JDBC事务管理的异常也是不一样的。

           具体如何使用JTA编程进行事务管理请参考cn.javass.spring.chapter9包下的TranditionalTransactionTest类。

           而在Spring中将采用一致的事务抽象进行控制和一致的异常控制,即面向PlatformTransactionManager接口编程来控制事务。

    Spring对编程式事务的支持

    Spring中的事务分为物理事务和逻辑事务;

    • 物理事务:就是底层数据库提供的事务支持,如JDBC或JTA提供的事务;
    • 逻辑事务:是Spring管理的事务,不同于物理事务,逻辑事务提供更丰富的控制,而且如果想得到Spring事务管理的好处,必须使用逻辑事务,因此在Spring中如果没特别强调一般就是逻辑事务;

    逻辑事务即支持非常低级别的控制,也有高级别解决方案:

    • 低级别解决方案:

             工具类:使用工具类获取连接(会话)和释放连接(会话),如使用org.springframework.jdbc.datasource包中的 DataSourceUtils 类来获取和释放具有逻辑事务功能的连接。当然对集成第三方ORM框架也提供了类似的工具类,如对Hibernate提供了SessionFactoryUtils工具类,JPA的EntityManagerFactoryUtils等,其他工具类都是使用类似***Utils命名;

    Java代码  收藏代码
    1. //获取具有Spring事务(逻辑事务)管理功能的连接  
    2. DataSourceUtils. getConnection(DataSource dataSource)  
    3. //释放具有Spring事务(逻辑事务)管理功能的连接  
    4. DataSourceUtils. releaseConnection(Connection con, DataSource dataSource)  
    5.    

             TransactionAwareDataSourceProxy使用该数据源代理类包装需要Spring事务管理支持的数据源,该包装类必须位于最外层,主要用于遗留项目中可能直接使用数据源获取连接和释放连接支持或希望在Spring中进行混合使用各种持久化框架时使用,其内部实际使用 DataSourceUtils 工具类获取和释放真正连接;

    Java代码  收藏代码
    1. <!--使用该方式包装数据源,必须在最外层,targetDataSource 知道目标数据源-->  
    2. <bean id="dataSourceProxy"  
    3. class="org.springframework.jdbc.datasource.  
    4. TransactionAwareDataSourceProxy">  
    5.     <property name="targetDataSource" ref="dataSource"/>  
    6. </bean>  

    通过如上方式包装数据源后,可以在项目中使用物理事务编码的方式来获得逻辑事务的支持,即支持直接从DataSource获取连接和释放连接,且这些连接自动支持Spring逻辑事务;

    使用TransactionTemplate

    TransactionTemplate模板类用于简化事务管理,事务管理由模板类定义,而具体操作需要通过TransactionCallback回调接口或TransactionCallbackWithoutResult回调接口指定,通过调用模板类的参数类型为TransactionCallback或TransactionCallbackWithoutResult的execute方法来自动享受事务管理。

    TransactionTemplate模板类使用的回调接口:

    • TransactionCallback:通过实现该接口的“T doInTransaction(TransactionStatus status) ”方法来定义需要事务管理的操作代码;
    • TransactionCallbackWithoutResult:继承TransactionCallback接口,提供“void doInTransactionWithoutResult(TransactionStatus status)”便利接口用于方便那些不需要返回值的事务操作代码。

    1、接下来演示一下TransactionTemplate模板类如何使用:

    Java代码  收藏代码
    1. @Test  
    2. public void testTransactionTemplate() {//位于TransactionTest类中  
    3.   jdbcTemplate.execute(CREATE_TABLE_SQL);  
    4.   TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);  
    5.   transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  
    6.   transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
    7.       @Override  
    8.       protected void doInTransactionWithoutResult(TransactionStatus status) {  
    9.          jdbcTemplate.update(INSERT_SQL, "test");  
    10.   }});  
    11.   jdbcTemplate.execute(DROP_TABLE_SQL);  
    12. }  
    • TransactionTemplate :通过new TransactionTemplate(txManager)创建事务模板类,其中构造器参数为PlatformTransactionManager实现,并通过其相应方法设置事务定义,如事务隔离级别、传播行为等,此处未指定传播行为,其默认为PROPAGATION_REQUIRED;
    • TransactionCallbackWithoutResult:此处使用不带返回的回调实现,其doInTransactionWithoutResult方法实现中定义了需要事务管理的操作;
    • transactionTemplate.execute():通过该方法执行需要事务管理的回调。

    这样是不是简单多了,没有事务管理代码,而是由模板类来完成事务管理。

    注:对于抛出Exception类型的异常且需要回滚时,需要捕获异常并通过调用status对象的setRollbackOnly()方法告知事务管理器当前事务需要回滚,如下所示:

    Java代码  收藏代码
    1. try {  
    2.     //业务操作  
    3. catch (Exception e) { //可使用具体业务异常代替  
    4.     status.setRollbackOnly();  
    5. }  

    事务属性

           事务属性通过TransactionDefinition接口实现定义,主要有事务隔离级别、事务传播行为、事务超时时间、事务是否只读。

           Spring提供TransactionDefinition接口默认实现DefaultTransactionDefinition,可以通过该实现类指定这些事务属性。

    • 事务隔离级别:用来解决并发事务时出现的问题,其使用TransactionDefinition中的静态变量来指定:

             ISOLATION_DEFAULT:默认隔离级别,即使用底层数据库默认的隔离级别;

             ISOLATION_READ_UNCOMMITTED:未提交读;

             ISOLATION_READ_COMMITTED:提交读,一般情况下我们使用这个;

             ISOLATION_REPEATABLE_READ:可重复读;

             ISOLATION_SERIALIZABLE:序列化。

    可以使用DefaultTransactionDefinition类的setIsolationLevel(TransactionDefinition. ISOLATION_READ_COMMITTED)来指定隔离级别,其中此处表示隔离级别为提交读,也可以使用或setIsolationLevelName(“ISOLATION_READ_COMMITTED”)方式指定,其中参数就是隔离级别静态变量的名字,但不推荐这种方式。

    • 事务传播行为:Spring管理的事务是逻辑事务,而且物理事务和逻辑事务最大差别就在于事务传播行为,事务传播行为用于指定在多个事务方法间调用时,事务是如何在这些方法间传播的,Spring共支持7种传播行为:

    Required:必须有逻辑事务,否则新建一个事务,使用PROPAGATION_REQUIRED指定,表示如果当前存在一个逻辑事务,则加入该逻辑事务,否则将新建一个逻辑事务,如图9-2和9-3所示;

     

    图9-2 Required传播行为

     

    图9-3 Required传播行为抛出异常情况

                  在前边示例中就是使用的Required传播行为:

    一、在调用userService对象的save方法时,此方法用的是Required传播行为且此时Spring事务管理器发现还没开启逻辑事务,因此Spring管理器觉得开启逻辑事务,

    二、在此逻辑事务中调用了addressService对象的save方法,而在save方法中发现同样用的是Required传播行为,因此使用该已经存在的逻辑事务;

    三、在返回到addressService对象的save方法,当事务模板类执行完毕,此时提交并关闭事务。

           因此userService对象的save方法和addressService的save方法属于同一个物理事务,如果发生回滚,则两者都回滚。

    接下来测试一下该传播行为如何执行吧:

    一、正确提交测试,如上一节的测试,在此不再演示;

    二、回滚测试,修改AddressServiceImpl的save方法片段:

    Java代码  收藏代码
    1. addressDao.save(address);  

    Java代码  收藏代码
    1. addressDao.save(address);  
    2. //抛出异常,将标识当前事务需要回滚  
    3. throw new RuntimeException();  

    二、修改UserServiceImpl的save方法片段:

    Java代码  收藏代码
    1. addressService.save(user.getAddress());  

    Java代码  收藏代码
    1. try {  
    2.     addressService.save(user.getAddress());//将在同一个事务内执行  
    3. catch (RuntimeException e) {  
    4. }  
    5.    

    如果该业务方法执行时事务被标记为回滚,则不管在此是否捕获该异常都将发生回滚,因为处于同一逻辑事务。

    三、修改测试方法片段:

    Java代码  收藏代码
    1. userService.save(user);  
    2. Assert.assertEquals(1, userService.countAll());  
    3. Assert.assertEquals(1, addressService.countAll());  

    为如下形式:

    Java代码  收藏代码
    1. try {  
    2.     userService.save(user);  
    3.     Assert.fail();  
    4. catch (RuntimeException e) {  
    5. }  
    6. Assert.assertEquals(0, userService.countAll());  
    7. Assert.assertEquals(0, addressService.countAll());  

    Assert断言中countAll方法都返回0,说明事务回滚了,即说明两个业务方法属于同一个物理事务,即使在userService对象的save方法中将异常捕获,由于addressService对象的save方法抛出异常,即事务管理器将自动标识当前事务为需要回滚。

    RequiresNew:创建新的逻辑事务,使用PROPAGATION_REQUIRES_NEW指定,表示每次都创建新的逻辑事务(物理事务也是不同的)如图9-4和9-5所示:

     

    图9-4 RequiresNew传播行为

     

    图9-5 RequiresNew传播行为并抛出异常

    接下来测试一个该传播行为如何执行吧:

    1、将如下获取事务模板方式

    Java代码  收藏代码
    1. TransactionTemplate transactionTemplate = TransactionTemplateUtils.getDefaultTransactionTemplate(txManager);  

           替换为如下形式,表示传播行为为RequiresNew:

    Java代码  收藏代码
    1. TransactionTemplate transactionTemplate = TransactionTemplateUtils.getTransactionTemplate(  
    2.         txManager,   
    3.         TransactionDefinition.PROPAGATION_REQUIRES_NEW,   
    4.         TransactionDefinition.ISOLATION_READ_COMMITTED);  

    2、执行如下测试,发现执行结果是正确的:


    Java代码  收藏代码
    1. userService.save(user);  
    2. Assert.assertEquals(1, userService.countAll());  
    3. Assert.assertEquals(1, addressService.countAll());  

    3、修改UserServiceImpl的save方法片段

    Java代码  收藏代码
    1. userDao.save(user);         
    2. user.getAddress().setUserId(user.getId());  
    3. addressService.save(user.getAddress());  

    为如下形式,表示userServiceImpl类的save方法将发生回滚,而AddressServiceImpl类的方法由于在抛出异常前执行,将成功提交事务到数据库:

    Java代码  收藏代码
    1. userDao.save(user);         
    2. user.getAddress().setUserId(user.getId());  
    3. addressService.save(user.getAddress());  
    4. throw new RuntimeException();  

    4、修改测试方法片段:

    Java代码  收藏代码
    1. userService.save(user);  
    2. Assert.assertEquals(1, userService.countAll());  
    3. Assert.assertEquals(1, addressService.countAll());  

    为如下形式:

    Java代码  收藏代码
    1. try {  
    2.     userService.save(user);  
    3.     Assert.fail();  
    4. catch (RuntimeException e) {  
    5. }  
    6. Assert.assertEquals(0, userService.countAll());  
    7. Assert.assertEquals(1, addressService.countAll());  

    Assert断言中调用userService对象countAll方法返回0,说明该逻辑事务作用域回滚,而调用addressService对象的countAll方法返回1,说明该逻辑事务作用域正确提交。因此这是不正确的行为,因为用户和地址应该是一一对应的,不应该发生这种情况,因此此处正确的传播行为应该是Required。

    该传播行为执行流程(正确提交情况):

    一、当执行userService对象的save方法时,由于传播行为是RequiresNew,因此创建一个新的逻辑事务(物理事务也是不同的);

    二、当执行到addressService对象的save方法时,由于传播行为是RequiresNew,因此首先暂停上一个逻辑事务并创建一个新的逻辑事务(物理事务也是不同的);

    三、addressService对象的save方法执行完毕后,提交逻辑事务(并提交物理事务)并重新恢复上一个逻辑事务,继续执行userService对象的save方法内的操作;

    四、最后userService对象的save方法执行完毕,提交逻辑事务(并提交物理事务);

    五、userService对象的save方法和addressService对象的save方法不属于同一个逻辑事务且也不属于同一个物理事务。

    Supports:支持当前事务,使用PROPAGATION_SUPPORTS指定,指如果当前存在逻辑事务,就加入到该逻辑事务,如果当前没有逻辑事务,就以非事务方式执行,如图9-6和9-7所示:

     

    图9-6 Required+Supports传播行为

     

    图9-7 Supports+Supports传播行为

    NotSupported:不支持事务,如果当前存在事务则暂停该事务,使用PROPAGATION_NOT_SUPPORTED指定,即以非事务方式执行,如果当前存在逻辑事务,就把当前事务暂停,以非事务方式执行,如图9-8和9-9所示:

     

           图9-8 Required+NotSupported传播行为

     

           图9-9 Supports+NotSupported传播行为

    Mandatory:必须有事务,否则抛出异常,使用PROPAGATION_MANDATORY指定,使用当前事务执行,如果当前没有事务,则抛出异常(IllegalTransactionStateException),如图9-10和9-11所示:

     

           图9-10 Required+Mandatory传播行为

     

           图9-11 Supports+Mandatory传播行为

    Never:不支持事务,如果当前存在是事务则抛出异常,使用PROPAGATION_NEVER指定,即以非事务方式执行,如果当前存在事务,则抛出异常(IllegalTransactionStateException),如图9-12和9-13所示:

     

           图9-12 Required+Never传播行为

          

    Nested:嵌套事务支持,使用PROPAGATION_NESTED指定,如果当前存在事务,则在嵌套事务内执行,如果当前不存在事务,则创建一个新的事务,嵌套事务使用数据库中的保存点来实现,即嵌套事务回滚不影响外部事务,但外部事务回滚将导致嵌套事务回滚,如图9-14和9-15所示:

     

           图9-14 Required+Nested传播行为

     

    图9-15 Nested+Nested传播行为

    Nested和RequiresNew的区别:

    1、  RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务;

    2、  Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而 RequiresNew由于都是全新的事务,所以之间是无关联的;

    3、  Nested使用JDBC 3的保存点实现,即如果使用低版本驱动将导致不支持嵌套事务。

    使用嵌套事务,必须确保具体事务管理器实现的nestedTransactionAllowed属性为true,否则不支持嵌套事务,如DataSourceTransactionManager默认支持,而HibernateTransactionManager默认不支持,需要我们来开启。

    对于事务传播行为我们只演示了Required和RequiresNew,其他传播行为类似,如果对这些事务传播行为不太会使用,请参考chapter9包下的TransactionTest测试类中的testPropagation方法,方法内有详细示例。

    • 事务超时:设置事务的超时时间,单位为秒,默认为-1表示使用底层事务的超时时间;

             使用如setTimeout(100)来设置超时时间,如果事务超时将抛出org.springframework.transaction.TransactionTimedOutException异常并将当前事务标记为应该回滚,即超时后事务被自动回滚;

             可以使用具体事务管理器实现的defaultTimeout属性设置默认的事务超时时间,如DataSourceTransactionManager. setDefaultTimeout(10)。

    • 事务只读:将事务标识为只读,只读事务不修改任何数据;

             对于JDBC只是简单的将连接设置为只读模式,对于更新将抛出异常;

             而对于一些其他ORM框架有一些优化作用,如在Hibernate中,Spring事务管理器将执行“session.setFlushMode(FlushMode.MANUAL)”即指定Hibernate会话在只读事务模式下不用尝试检测和同步持久对象的状态的更新。

             如果使用设置具体事务管理的validateExistingTransaction属性为true(默认false),将确保整个事务传播链都是只读或都不是只读,如图9-16是正确的事务只读设置,而图9-17是错误的事务只读设置:

     

    图9-16 正确的事务只读设置

     

    图9-17 错误的事务只读设置

    如图10-17,对于错误的事务只读设置将抛出IllegalTransactionStateException异常,并伴随“Participating transaction with definition [……] is not marked as read-only……”信息,表示参与的事务只读属性设置错误。

    大家有没有感觉到编程式实现事务管理是不是很繁琐冗长,重复,而且是侵入式的,因此发展到这Spring决定使用配置方式实现事务管理。

     

     

      配置方式实现事务管理

    在Spring2.x之前为了解决编程式事务管理的各种不好问题,Spring提出使用配置方式实现事务管理,配置方式利用代理机制实现,即使有TransactionProxyFactoryBean类来为目标类代理事务管理。

    接下来演示一下具体使用吧:

    1、重新定义业务类实现,在业务类中无需显示的事务管理代码:

    Java代码  收藏代码
    1. package cn.javass.spring.chapter9.service.impl;  
    2. //省略import  
    3. public class ConfigAddressServiceImpl implements IAddressService {  
    4.     private IAddressDao addressDao;  
    5.     public void setAddressDao(IAddressDao addressDao) {  
    6.         this.addressDao = addressDao;  
    7.     }  
    8.     @Override  
    9.     public void save(final AddressModel address) {  
    10.         addressDao.save(address);  
    11.     }  
    12.     //countAll方法实现不变  
    13. }  
    Java代码  收藏代码
    1. package cn.javass.spring.chapter9.service.impl;  
    2. //省略import  
    3. public class ConfigUserServiceImpl implements IUserService {  
    4.     private IUserDao userDao;  
    5.     private IAddressService addressService;  
    6.     public void setUserDao(IUserDao userDao) {  
    7.         this.userDao = userDao;  
    8.     }  
    9.     public void setAddressService(IAddressService addressService) {  
    10.         this.addressService = addressService;  
    11.     }  
    12.     @Override  
    13.     public void save(final UserModel user) {  
    14.         userDao.save(user);  
    15.         user.getAddress().setUserId(user.getId());  
    16.         addressService.save(user.getAddress());  
    17.     }  
    18.     //countAll方法实现不变  
    19. }  

    从以上业务类中可以看出,没有事务管理的代码,即没有侵入式的代码。

     

    2、在chapter9/service/applicationContext-service.xml配置文件中添加如下配置:

    2.1、首先添加目标类定义:

    Java代码  收藏代码
    1. <bean id="targetUserService" class="cn.javass.spring.chapter9.service.impl.ConfigUserServiceImpl">  
    2.     <property name="userDao" ref="userDao"/>  
    3.     <property name="addressService" ref="targetAddressService"/>  
    4. </bean>  
    5. <bean id="targetAddressService" class="cn.javass.spring.chapter9.service.impl.ConfigAddressServiceImpl">  
    6.     <property name="addressDao" ref="addressDao"/>  
    7. </bean>  

    2.2、配置TransactionProxyFactoryBean类:

    Java代码  收藏代码
    1. <bean id="transactionProxyParent" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"  abstract="true">  
    2.     <property name="transactionManager" ref="txManager"/>  
    3.     <property name="transactionAttributes">  
    4.     <props>  
    5.             <prop key="save*">  
    6.                       PROPAGATION_REQUIRED,  
    7.                       ISOLATION_READ_COMMITTED,  
    8.                       timeout_10,  
    9.                       -Exception,  
    10.                       +NoRollBackException  
    11.            </prop>  
    12.            <prop key="*">  
    13.                       PROPAGATION_REQUIRED,  
    14.                       ISOLATION_READ_COMMITTED,  
    15.                       readOnly  
    16.            </prop>  
    17.         </props>  
    18. </property>  
    19. </bean>  
    20.    
    • TransactionProxyFactoryBean用于为目标业务类创建代理的Bean;
    • abstract="true"表示该Bean是抽象的,用于去除重复配置;
    • transactionManager事务管理器定义;
    • transactionAttributes表示事务属性定义:
    • PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,timeout_10,-Exception,+NoRollBackException事务属性定义,Required传播行为,提交读隔离级别,事务超时时间为10秒,将对所有Exception异常回滚,而对于抛出NoRollBackException异常将不发生回滚而是提交;
    • PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,readOnly事务属性定义,Required传播行为,提交读隔离级别,事务是只读的,且只对默认的RuntimeException异常回滚;
    • <prop key="save*">表示将代理以save开头的方法,即当执行到该方法时会为该方法根据事务属性配置来开启/关闭事务;
    • <prop key="*">表示将代理其他所有方法,但需要注意代理方式,默认是JDK代理,只有public方法能代理;

    注:事务属性的传播行为和隔离级别使用TransactionDefinition静态变量名指定;事务超时使用“timeout_超时时间”指定,事务只读使用“readOnly”指定,需要回滚的异常使用“-异常”指定,不需要回滚的异常使用“+异常”指定,默认只对RuntimeException异常回滚。

    需要特别注意“-异常”和“+异常”中“异常”只是真实异常的部分名,内部使用如下方式判断:

    Java代码  收藏代码
    1. //真实抛出的异常.name.indexOf(配置中指定的需要回滚/不回滚的异常名)  
    2. exceptionClass.getName().indexOf(this.exceptionName)  
    3.    

           因此异常定义时需要特别注意,配置中定义的异常只是真实异常的部分名。

     

    2.3、定义代理Bean

    Java代码  收藏代码
    1. <bean id="proxyUserService" parent="transactionProxyParent">  
    2.     <property name="target" ref="targetUserService"/>  
    3. </bean>  
    4. <bean id="proxyAddressService" parent="transactionProxyParent">  
    5.     <property name="target" ref="targetAddressService"/>  
    6. </bean>  

    代理Bean通过集成抽象Bean“transactionProxyParent”,并通过target属性设置目标Bean,在实际使用中应该使用该代理Bean。

    3、修改测试方法并测试该配置方式是否好用:

    将TransactionTest 类的testServiceTransaction测试方法拷贝一份命名为testConfigTransaction:

    并在testConfigTransaction测试方法内将:

    Java代码  收藏代码
    1. IUserService userService =  
    2. ctx2.getBean("userService", IUserService.class);  
    3. IAddressService addressService =  
    4. ctx2.getBean("addressService", IAddressService.class);  

    替换为:

    Java代码  收藏代码
    1. IUserService userService =  
    2. ctx2.getBean("proxyUserService ", IUserService.class);  
    3. IAddressService addressService =  
    4. ctx2.getBean("proxyAddressService ", IAddressService.class);  

    4、执行测试,测试正常通过,说明该方式能正常工作,当调用save方法时将匹配到“<prop key="save*">”定义,而countAll将匹配到“<prop key="save*">”定义,底层代理会应用相应定义中的事务属性来创建或关闭事务。

    注:在代理模式下,默认只有通过代理对象调用的方法才能应用相应的事务属性,而在目标方法内的“自我调用”是不会应用相应的事务属性的,即被调用方法不会应用相应的事务属性,而是使用调用方法的事务属性。

    声明式事务概述

           从上节编程式实现事务管理可以深刻体会到编程式事务的痛苦,即使通过代理配置方式也是不小的工作量。

           本节将介绍声明式事务支持,使用该方式后最大的获益是简单,事务管理不再是令人痛苦的,而且此方式属于无侵入式,对业务逻辑实现无影响。

           接下来先来看看声明式事务如何实现吧。

    声明式实现事务管理

    1、定义业务逻辑实现,此处使用ConfigUserServiceImpl和ConfigAddressServiceImpl

    2、定义配置文件(chapter9/service/ applicationContext-service-declare.xml):

    2.1、XML命名空间定义,定义用于事务支持的tx命名空间和AOP支持的aop命名空间:

    Java代码  收藏代码
    1. <beans xmlns="http://www.springframework.org/schema/beans"  
    2.       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    3.     xmlns:tx="http://www.springframework.org/schema/tx"  
    4.       xmlns:aop="http://www.springframework.org/schema/aop"  
    5.       xsi:schemaLocation="  
    6.           http://www.springframework.org/schema/beans  
    7.           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
    8.        http://www.springframework.org/schema/tx  
    9.        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd  
    10.           http://www.springframework.org/schema/aop  
    11.           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">  

    2.2、业务实现配置,非常简单,使用以前定义的非侵入式业务实现:

    Java代码  收藏代码
    1. <bean id="userService" class="cn.javass.spring.chapter9.service.impl.ConfigUserServiceImpl">  
    2.     <property name="userDao" ref="userDao"/>  
    3.     <property name="addressService" ref="addressService"/>  
    4. </bean>  
    5. <bean id="addressService" class="cn.javass.spring.chapter9.service.impl.ConfigAddressServiceImpl">  
    6.     <property name="addressDao" ref="addressDao"/>  
    7. </bean>  

    2.3、事务相关配置:

    Java代码  收藏代码
    1. <tx:advice id="txAdvice" transaction-manager="txManager">  
    2.     <tx:attributes>  
    3.         <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED"/>  
    4.         <tx:method name="*" propagation="REQUIRED" isolation="READ_COMMITTED" read-only="true"/>  
    5.     </tx:attributes>  
    6. </tx:advice>  
    Java代码  收藏代码
    1. <aop:config>  
    2.     <aop:pointcut id="serviceMethod" expression="execution(* cn..chapter9.service..*.*(..))"/>  
    3.     <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice"/>  
    4. </aop:config>  
    • <tx:advice>:事务通知定义,用于指定事务属性,其中“transaction-manager”属性指定事务管理器,并通过< tx:attributes >指定具体需要拦截的方法;
    •  <tx:method name="save*">:表示将拦截以save开头的方法,被拦截的方法将应用配置的事务属性:propagation="REQUIRED"表示传播行为是Required,isolation="READ_COMMITTED"表示隔离级别是提交读;
    • <tx:method name="*">:表示将拦截其他所有方法,被拦截的方法将应用配置的事务属性:propagation="REQUIRED"表示传播行为是Required,isolation="READ_COMMITTED"表示隔离级别是提交读,read-only="true"表示事务只读;
    • <aop:config>:AOP相关配置:
    • <aop:pointcut/>:切入点定义,定义名为"serviceMethod"的aspectj切入点,切入点表达式为"execution(* cn..chapter9.service..*.*(..))"表示拦截cn包及子包下的chapter9. service包及子包下的任何类的任何方法;
    • <aop:advisor>:Advisor定义,其中切入点为serviceMethod,通知为txAdvice。

    从配置中可以看出,将对cn包及子包下的chapter9. service包及子包下的任何类的任何方法应用“txAdvice”通知指定的事务属性。

    3、修改测试方法并测试该配置方式是否好用:

    将TransactionTest 类的testServiceTransaction测试方法拷贝一份命名为testDeclareTransaction:

    并在testDeclareTransaction测试方法内将:

    Java代码  收藏代码
    1. classpath:chapter9/service/applicationContext-service.xml"  

    替换为:

    Java代码  收藏代码
    1. classpath:chapter9/service/applicationContext-service-declare.xml"  

    4、执行测试,测试正常通过,说明该方式能正常工作,当调用save方法时将匹配到事务通知中定义的“<tx:method name="save*">”中指定的事务属性,而调用countAll方法时将匹配到事务通知中定义的“<tx:method name="*">”中指定的事务属性。

    声明式事务是如何实现事务管理的呢?还记不记得TransactionProxyFactoryBean实现配置式事务管理,配置式事务管理是通过代理方式实现,而声明式事务管理同样是通过AOP代理方式实现。

    声明式事务通过AOP代理方式实现事务管理,利用环绕通知TransactionInterceptor实现事务的开启及关闭,而TransactionProxyFactoryBean内部也是通过该环绕通知实现的,因此可以认为是<tx:tags/>帮你定义了TransactionProxyFactoryBean,从而简化事务管理。

    了解了实现方式后,接下来详细学习一下配置吧:

      <tx:advice/>配置详解

           声明式事务管理通过配置<tx:advice/>来定义事务属性,配置方式如下所示:

    Java代码  收藏代码
    1. <tx:advice id="……" transaction-manager="……">  
    2. <tx:attributes>  
    3.         <tx:method name="……"  
    4.                            propagation=" REQUIRED"  
    5.                            isolation="READ_COMMITTED"  
    6.                            timeout="-1"  
    7.                            read-only="false"  
    8.                            no-rollback-for=""   
    9.                            rollback-for=""/>  
    10.         ……  
    11.     </tx:attributes>  
    12. </tx:advice>  
    • <tx:advice>id用于指定此通知的名字, transaction-manager用于指定事务管理器,默认的事务管理器名字为“transactionManager”;
    • <tx:method>:用于定义事务属性即相关联的方法名;

             name定义与事务属性相关联的方法名,将对匹配的方法应用定义的事务属性,可以使用“*”通配符来匹配一组或所有方法,如“save*”将匹配以save开头的方法,而“*”将匹配所有方法;

             propagation:事务传播行为定义,默认为“REQUIRED”,表示Required,其值可以通过TransactionDefinition的静态传播行为变量的“PROPAGATION_”后边部分指定,如“TransactionDefinition.PROPAGATION_REQUIRED”可以使用“REQUIRED”指定;

             isolation:事务隔离级别定义;默认为“DEFAULT”,其值可以通过TransactionDefinition的静态隔离级别变量的“ISOLATION_”后边部分指定,如“TransactionDefinition. ISOLATION_DEFAULT”可以使用“DEFAULT”指定:

             timeout事务超时时间设置,单位为秒,默认-1,表示事务超时将依赖于底层事务系统;

             read-only事务只读设置,默认为false,表示不是只读;

             rollback-for需要触发回滚的异常定义,以“,”分割,默认任何RuntimeException 将导致事务回滚,而任何Checked Exception 将不导致事务回滚;异常名字定义和TransactionProxyFactoryBean中含义一样

             no-rollback-for不被触发进行回滚的 Exception(s);以“,”分割;异常名字定义和TransactionProxyFactoryBean中含义一样;

    记不记得在配置方式中为了解决“自我调用”而导致的不能设置正确的事务属性问题,使用“((IUserService)AopContext.currentProxy()).otherTransactionMethod()方式解决,在声明式事务要得到支持需要使用<aop:config expose-proxy="true">来开启。

     

    多事务语义配置及最佳实践

           什么是多事务语义?说白了就是为不同的Bean配置不同的事务属性,因为我们项目中不可能就几个Bean,而可能很多,这可能需要为Bean分组,为不同组的Bean配置不同的事务语义。在Spring中,可以通过配置多切入点和多事务通知并通过不同方式组合使用即可。

           1、首先看下声明式事务配置的最佳实践吧:

    Java代码  收藏代码
    1. <tx:advice id="txAdvice" transaction-manager="txManager">  
    2. <tx:attributes>  
    3.            <tx:method name="save*" propagation="REQUIRED" />  
    4.            <tx:method name="add*" propagation="REQUIRED" />  
    5.            <tx:method name="create*" propagation="REQUIRED" />  
    6.            <tx:method name="insert*" propagation="REQUIRED" />  
    7.            <tx:method name="update*" propagation="REQUIRED" />  
    8.            <tx:method name="merge*" propagation="REQUIRED" />  
    9.            <tx:method name="del*" propagation="REQUIRED" />  
    10.            <tx:method name="remove*" propagation="REQUIRED" />  
    11.            <tx:method name="put*" propagation="REQUIRED" />  
    12.            <tx:method name="get*" propagation="SUPPORTS" read-only="true" />  
    13.            <tx:method name="count*" propagation="SUPPORTS" read-only="true" />  
    14.           <tx:method name="find*" propagation="SUPPORTS" read-only="true" />  
    15.           <tx:method name="list*" propagation="SUPPORTS" read-only="true" />  
    16.           <tx:method name="*" propagation="SUPPORTS" read-only="true" />  
    17.        </tx:attributes>  
    18. </tx:advice>  
    19. <aop:config>  
    20.        <aop:pointcut id="txPointcut" expression="execution(* cn.javass..service.*.*(..))" />  
    21.        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />  
    22. </aop:config>  

    该声明式事务配置可以应付常见的CRUD接口定义,并实现事务管理,我们只需修改切入点表达式来拦截我们的业务实现从而对其应用事务属性就可以了,如果还有更复杂的事务属性直接添加即可,即

    如果我们有一个batchSaveOrUpdate方法需要“REQUIRES_NEW”事务传播行为,则直接添加如下配置即可:

    Java代码  收藏代码
    1. <tx:method name="batchSaveOrUpdate" propagation="REQUIRES_NEW" />  

    2、接下来看一下多事务语义配置吧,声明式事务最佳实践中已经配置了通用事务属性,因此可以针对需要其他事务属性的业务方法进行特例化配置:

    Java代码  收藏代码
    1. <tx:advice id="noTxAdvice" transaction-manager="txManager">  
    2.     <tx:attributes>  
    3.            <tx:method name="*" propagation="NEVER" />  
    4.     </tx:attributes>  
    5. </tx:advice>  
    6. <aop:config>  
    7.        <aop:pointcut id="noTxPointcut" expression="execution(* cn.javass..util.*.*())" />  
    8.        <aop:advisor advice-ref="noTxPointcut" pointcut-ref="noTxAdvice" />  
    9. </aop:config>  

           该声明将对切入点匹配的方法所在事务应用“Never”传播行为。

           多事务语义配置时,切入点一定不要叠加,否则将应用两次事务属性,造成不必要的错误及麻烦。

     

     @Transactional实现事务管理

    对声明式事务管理,Spring提供基于@Transactional注解方式来实现,但需要Java 5+。

    注解方式是最简单的事务配置方式,可以直接在Java源代码中声明事务属性,且对于每一个业务类或方法如果需要事务都必须使用此注解。

    接下来学习一下注解事务的使用吧:

    1、定义业务逻辑实现:

    Java代码  收藏代码
    1. package cn.javass.spring.chapter9.service.impl;  
    2. //省略import  
    3. public class AnnotationUserServiceImpl implements IUserService {  
    4.     private IUserDao userDao;  
    5.     private IAddressService addressService;  
    6.     public void setUserDao(IUserDao userDao) {  
    7.         this.userDao = userDao;  
    8.     }  
    9.     public void setAddressService(IAddressService addressService) {  
    10.         this.addressService = addressService;  
    11.     }  
    12.     @Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)  
    13.     @Override  
    14.     public void save(final UserModel user) {  
    15.         userDao.save(user);  
    16.         user.getAddress().setUserId(user.getId());  
    17.         addressService.save(user.getAddress());  
    18.     }  
    19.     @Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED, readOnly=true)  
    20.     @Override  
    21.     public int countAll() {  
    22.         return userDao.countAll();  
    23.     }  
    24. }  

    2、定义配置文件(chapter9/service/ applicationContext-service-annotation.xml):

    2.1、XML命名空间定义,定义用于事务支持的tx命名空间和AOP支持的aop命名空间:

    Java代码  收藏代码
    1. <beans xmlns="http://www.springframework.org/schema/beans"  
    2.       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    3.     xmlns:tx="http://www.springframework.org/schema/tx"  
    4.       xmlns:aop="http://www.springframework.org/schema/aop"  
    5.       xsi:schemaLocation="  
    6.           http://www.springframework.org/schema/beans  
    7.           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
    8.        http://www.springframework.org/schema/tx  
    9.        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd  
    10.           http://www.springframework.org/schema/aop  
    11.           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">  

    2.2、业务实现配置,非常简单,使用以前定义的非侵入式业务实现:

    Java代码  收藏代码
    1. <bean id="userService" class="cn.javass.spring.chapter9.service.impl.ConfigUserServiceImpl">  
    2.     <property name="userDao" ref="userDao"/>  
    3.     <property name="addressService" ref="addressService"/>  
    4. </bean>  
    5. <bean id="addressService" class="cn.javass.spring.chapter9.service.impl.ConfigAddressServiceImpl">  
    6.     <property name="addressDao" ref="addressDao"/>  
    7. </bean>  
    8.    

    2.3、事务相关配置:

    Java代码  收藏代码
    1. <tx:annotation-driven transaction-manager="txManager"/>   

    使用如上配置已支持声明式事务。

    3、修改测试方法并测试该配置方式是否好用:

    将TransactionTest 类的testServiceTransaction测试方法拷贝一份命名为testAnntationTransactionTest:

    将测试代码片段:

    Java代码  收藏代码
    1. classpath:chapter9/service/applicationContext-service.xml"  

    替换为:

    Java代码  收藏代码
    1. classpath:chapter9/service/applicationContext-service-annotation.xml"  

    将测试代码段

    Java代码  收藏代码
    1. userService.save(user);  

    替换为:

    Java代码  收藏代码
    1. try {  
    2.     userService.save(user);  
    3.     Assert.fail();  
    4. catch (RuntimeException e) {  
    5. }  
    6. Assert.assertEquals(0, userService.countAll());  
    7. Assert.assertEquals(0, addressService.countAll());  
    8.    

    4、执行测试,测试正常通过,说明该方式能正常工作,因为在AnnotationAddressServiceImpl类的save方法中抛出异常,因此事务需要回滚,所以两个countAll操作都返回0。

      @Transactional配置详解

           Spring提供的<tx:annotation-driven/>用于开启对注解事务管理的支持,从而能识别Bean类上的@Transactional注解元数据,其具有以下属性:

    • transaction-manager:指定事务管理器名字,默认为transactionManager,当使用其他名字时需要明确指定;
    • proxy-target-class:表示将使用的代码机制,默认false表示使用JDK代理,如果为true将使用CGLIB代理
    • order:定义事务通知顺序,默认Ordered.LOWEST_PRECEDENCE,表示将顺序决定权交给AOP来处理。

    Spring使用@Transaction来指定事务属性,可以在接口、类或方法上指定,如果类和方法上都指定了@Transaction,则方法上的事务属性被优先使用,具体属性如下:

    • value:指定事务管理器名字,默认使用<tx:annotation-driven/>指定的事务管理器,用于支持多事务管理器环境;
    • propagation:指定事务传播行为,默认为Required,使用Propagation.REQUIRED指定;
    • isolation:指定事务隔离级别,默认为“DEFAULT”,使用Isolation.DEFAULT指定;
    • readOnly:指定事务是否只读,默认false表示事务非只读;
    • timeout:指定事务超时时间,以秒为单位,默认-1表示事务超时将依赖于底层事务系统;
    • rollbackFor:指定一组异常类,遇到该类异常将回滚事务;
    • rollbackForClassname:指定一组异常类名字,其含义与<tx:method>中的rollback-for属性语义完全一样;
    • noRollbackFor:指定一组异常类,即使遇到该类异常也将提交事务,即不回滚事务;
    • noRollbackForClassname:指定一组异常类名字,其含义与<tx:method>中的no-rollback-for属性语义完全一样;

    Spring提供的@Transaction注解事务管理内部同样利用环绕通知TransactionInterceptor实现事务的开启及关闭。

    使用@Transactional注解事务管理需要特别注意以下几点:

    • 如果在接口、实现类或方法上都指定了@Transactional 注解,则优先级顺序为方法>实现类>接口;
    • 建议只在实现类或实现类的方法上使用@Transactional,而不要在接口上使用,这是因为如果使用JDK代理机制是没问题,因为其使用基于接口的代理;而使用使用CGLIB代理机制时就会遇到问题,因为其使用基于类的代理而不是接口,这是因为接口上的@Transactional注解是“不能继承的”
    • 在Spring代理机制下(不管是JDK动态代理还是CGLIB代理),“自我调用”同样不会应用相应的事务属性,其语义和<tx:tags>中一样;
    • 默认只对RuntimeException异常回滚;
    • 在使用Spring代理时,默认只有在public可见度的方法的@Transactional 注解才是有效的,其它可见度(protected、private、包可见)的方法上即使有@Transactional 注解也不会应用这些事务属性的,Spring也不会报错,如果你非要使用非公共方法注解事务管理的话,可考虑使用AspectJ。

      与其他AOP通知协作

           Spring声明式事务实现其实就是Spring AOP+线程绑定实现,利用AOP实现开启和关闭事务,利用线程绑定(ThreadLocal)实现跨越多个方法实现事务传播。

           由于我们不可能只使用一个事务通知,可能还有其他类型事务通知,而且如果这些通知中需要事务支持怎么办?这就牵扯到通知执行顺序的问题上了,因此如果可能与其他AOP通知协作的话,而且这些通知中需要使用声明式事务管理支持,事务通知应该具有最高优先级。

      声明式or编程式

           编程式事务时不推荐的,即使有很少事务操作,Spring发展到现在,没有理由使用编程式事务,只有在为了深入理解Spring事务管理才需要学习编程式事务使用。

           推荐使用声明式事务,而且强烈推荐使用<tx:tags>方式的声明式事务,因为其是无侵入代码的,可以配置模板化的事务属性并运用到多个项目中。

           而@Transaction注解事务,可以使用,不过作者更倾向于使用<tx:tags>声明式事务。

           能保证项目正常工作的事务配置就是最好的。

    9.4.11  混合事务管理

    所谓混合事务管理就是混合多种数据访问技术使用,如混合使用Spring JDBC + Hibernate,接下来让我们学习一下常见混合事务管理:

    1、  Hibernate + Spring JDBC/iBATIS:使用HibernateTransactionManager即可支持;

    2、  JPA + Spring JDBC/iBATIS:使用JpaTransactionManager即可支持;

    3、  JDO + Spring JDBC/iBATIS:使用JtaTransactionManager即可支持;

    混合事务管理最大问题在于如果我们使用第三方ORM框架,如Hibernate,会遇到一级及二级缓存问题,尤其是二级缓存可能造成如使用Spring JDBC和Hibernate查询出来的数据不一致等。

    因此不建议使用这种混合使用和混合事务管理。

  • 相关阅读:
    BERT在语义相似度计算中的应用(一)
    Debian 10.6 安装 oracle 19.3c 数据库database软件:传统的zip安装包
    Spring应用消费REST服务
    使用Spring Data自动创建repository实现及自动定义API实现
    SpringBoot中使用Spring profile进行配置【谨慎使用,参见SpringBoot 2.4.0发布文档】
    为SpringBoot声明配置属性的元数据
    【转】Winform中textBox通过正则表达式限制只能输入数字且是两位小数
    【转】WinForm中TextBox只能输入数字
    【转】C# winform窗体间传值(使用委托或事件)
    【转】Visual Studio2019报错/plugin.vs.js,行:1074,错误:缺少标识符、字符串或数字的解决方法
  • 原文地址:https://www.cnblogs.com/doudouxiaoye/p/5782327.html
Copyright © 2011-2022 走看看