当前位置:   article > 正文

【第九章】 Spring的事务 之 9.3 编程式事务 ——跟我学spring3

编程式事务

 

9.3  编程式事务

9.3.1  编程式事务概述

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

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

 

java代码:
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接口编程来控制事务。

 

9.3.1    Spring对编程式事务的支持

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

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

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

  • 低级别解决方案:

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

 

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

 

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

 

java代码:
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逻辑事务;

  • 高级别解决方案:

         模板类:使用Spring提供的模板类,如JdbcTemplate、HibernateTemplate和JpaTemplate模板类等,而这些模板类内部其实是使用了低级别解决方案中的工具类来管理连接或会话;

 

Spring提供两种编程式事务支持:直接使用PlatformTransactionManager实现和使用TransactionTemplate模板类,用于支持逻辑事务管理。

如果采用编程式事务推荐使用TransactionTemplate模板类和高级别解决方案。

 

9.3.3  使用PlatformTransactionManager

首先让我们看下如何使用PlatformTransactionManager实现来进行事务管理:

1、数据源定义,此处使用第7章的配置文件,即“chapter7/ applicationContext-resources.xml”文件。

 

2、事务管理器定义(chapter9/applicationContext-jdbc.xml):

 

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

 

3、 准备测试环境:

3.1、首先准备测试时使用的SQL

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9;  
  2. //省略import  
  3. public class TransactionTest {  
  4.     //id自增主键从0开始  
  5.     private static final String CREATE_TABLE_SQL = "create table test" +  
  6.     "(id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " +  
  7.     "name varchar(100))";  
  8.     private static final String DROP_TABLE_SQL = "drop table test";  
  9.     private static final String INSERT_SQL = "insert into test(name) values(?)";  
  10.     private static final String COUNT_SQL = "select count(*) from test";  
  11.     ……  
  12. }  

 

3.2、初始化Spring容器

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9;  
  2. //省略import  
  3. public class TransactionTest {  
  4.     private static ApplicationContext ctx;  
  5.     private static PlatformTransactionManager txManager;  
  6.     private static DataSource dataSource;  
  7.     private static JdbcTemplate jdbcTemplate;  
  8.     ……  
  9.     @BeforeClass  
  10.     public static void setUpClass() {  
  11.         String[] configLocations = new String[] {  
  12.                 "classpath:chapter7/applicationContext-resources.xml",  
  13.                 "classpath:chapter9/applicationContext-jdbc.xml"};  
  14.         ctx = new ClassPathXmlApplicationContext(configLocations);  
  15.         txManager = ctx.getBean(PlatformTransactionManager.class);  
  16.         dataSource = ctx.getBean(DataSource.class);  
  17.         jdbcTemplate = new JdbcTemplate(dataSource);  
  18.     }   
  19.     ……  
  20. }  

 

3.3、使用高级别方案JdbcTemplate来进行事务管理器测试:

 

java代码:
Java代码    收藏代码
  1. @Test  
  2. public void testPlatformTransactionManager() {  
  3.     DefaultTransactionDefinition def = new DefaultTransactionDefinition();  
  4.     def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  
  5.     def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);  
  6.     TransactionStatus status = txManager.getTransaction(def);  
  7.     jdbcTemplate.execute(CREATE_TABLE_SQL);  
  8.     try {  
  9.         jdbcTemplate.update(INSERT_SQL, "test");  
  10.         txManager.commit(status);  
  11.     } catch (RuntimeException e) {  
  12.         txManager.rollback(status);  
  13.     }  
  14.     jdbcTemplate.execute(DROP_TABLE_SQL);  
  15. }  
  • DefaultTransactionDefinition事务定义,定义如隔离级别、传播行为等,即在本示例中隔离级别为ISOLATION_READ_COMMITTED(提交读),传播行为为PROPAGATION_REQUIRED(必须有事务支持,即如果当前没有事务,就新建一个事务,如果已经存在一个事务中,就加入到这个事务中)。
  • TransactionStatus事务状态类,通过PlatformTransactionManager的getTransaction方法根据事务定义获取;获取事务状态后,Spring根据传播行为来决定如何开启事务;
  • JdbcTemplate:通过JdbcTemplate对象执行相应的SQL操作,且自动享受到事务支持,注意事务是线程绑定的,因此事务管理器可以运行在多线程环境;
  • txManager.commit(status):提交status对象绑定的事务;
  • txManager.rollback(status):当遇到异常时回滚status对象绑定的事务。

 

3.4、使用低级别解决方案来进行事务管理器测试:

 

java代码:
Java代码    收藏代码
  1. @Test  
  2. public void testPlatformTransactionManagerForLowLevel1() {  
  3. DefaultTransactionDefinition def = new DefaultTransactionDefinition();      def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);      def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);  
  4.   TransactionStatus status = txManager.getTransaction(def);  
  5.   Connection conn = DataSourceUtils.getConnection(dataSource);  
  6.   try {  
  7.       conn.prepareStatement(CREATE_TABLE_SQL).execute();  
  8.       PreparedStatement pstmt = conn.prepareStatement(INSERT_SQL);  
  9.       pstmt.setString(1"test");  
  10.       pstmt.execute();  
  11.       conn.prepareStatement(DROP_TABLE_SQL).execute();  
  12.       txManager.commit(status);  
  13.   } catch (Exception e) {  
  14.       status.setRollbackOnly();  
  15.       txManager.rollback(status);  
  16.   } finally {  
  17.       DataSourceUtils.releaseConnection(conn, dataSource);  
  18. }  
  19. }  
  20.    

 

低级别方案中使用DataSourceUtils获取和释放连接,使用txManager开管理事务,而且面向JDBC编程,比起模板类方式来繁琐和复杂的多,因此不推荐使用该方式。在此就不介绍数据源代理类使用了,需要请参考platformTransactionManagerForLowLevelTest2测试方法。

 

到此事务管理是不是还很繁琐?必须手工提交或回滚事务,有没有更好的解决方案呢?Spring提供了TransactionTemplate模板类来简化事务管理。

 

9.3.4  使用TransactionTemplate

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

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

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

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

 

java代码:
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代码:
Java代码    收藏代码
  1. try {  
  2.     //业务操作  
  3. catch (Exception e) { //可使用具体业务异常代替  
  4.     status.setRollbackOnly();  
  5. }  

 

 

2、前边已经演示了JDBC事务管理,接下来演示一下JTA分布式事务管理:

 

java代码:
Java代码    收藏代码
  1. @Test  
  2. public void testJtaTransactionTemplate() {  
  3.     String[] configLocations = new String[] {  
  4.       "classpath:chapter9/applicationContext-jta-derby.xml"};  
  5.     ctx = new ClassPathXmlApplicationContext(configLocations);  
  6.     final PlatformTransactionManager jtaTXManager = ctx.getBean(PlatformTransactionManager.class);  
  7.     final DataSource derbyDataSource1 = ctx.getBean("dataSource1", DataSource.class);  
  8.     final DataSource derbyDataSource2 = ctx.getBean("dataSource2", DataSource.class);  
  9.     final JdbcTemplate jdbcTemplate1 = new JdbcTemplate(derbyDataSource1);  
  10.     final JdbcTemplate jdbcTemplate2 = new JdbcTemplate(derbyDataSource2);  
  11.     TransactionTemplate transactionTemplate = new TransactionTemplate(jtaTXManager);   
  12.     transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  
  13.     jdbcTemplate1.update(CREATE_TABLE_SQL);  
  14.     int originalCount = jdbcTemplate1.queryForInt(COUNT_SQL);  
  15.     try {  
  16.         transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
  17.             @Override  
  18.             protected void doInTransactionWithoutResult(TransactionStatus status) {  
  19.                 jdbcTemplate1.update(INSERT_SQL, "test");  
  20.                //因为数据库2没有创建数据库表因此会回滚事务  
  21.               jdbcTemplate2.update(INSERT_SQL, "test");  
  22.           }});  
  23.     } catch (RuntimeException e) {  
  24.         int count = jdbcTemplate1.queryForInt(COUNT_SQL);  
  25.         Assert.assertEquals(originalCount, count);  
  26.     }  
  27.     jdbcTemplate1.update(DROP_TABLE_SQL);  
  28. }  
  • 配置文件:使用此前定义的chapter9/applicationContext-jta-derby.xml;
  • jtaTXManager: JTA事务管理器;
  • derbyDataSource1和derbyDataSource2:derby数据源1和derby数据源2;
  • jdbcTemplate1和jdbcTemplate2:分别使用derbyDataSource1和derbyDataSource2构造的JDBC模板类;
  • transactionTemplate:使用jtaTXManager事务管理器的事务管理模板类,其隔离级别为提交读,传播行为默认为PROPAGATION_REQUIRED(必须有事务支持,即如果当前没有事务,就新建一个事务,如果已经存在一个事务中,就加入到这个事务中);
  • jdbcTemplate1.update(CREATE_TABLE_SQL):此处只有derbyDataSource1所代表的数据库创建了“test”表,而derbyDataSource2所代表的数据库没有此表;
  • TransactionCallbackWithoutResult:在此接口实现中定义了需要事务支持的操作:

         jdbcTemplate1.update(INSERT_SQL, "test"):表示向数据库1中的test表中插入数据;

         jdbcTemplate2.update(INSERT_SQL, "test"):表示向数据库2中的test表中插入数据,但数据库2没有此表将抛出异常,且JTA分布式事务将回滚;

  • Assert.assertEquals(originalCount, count):用来验证事务是否回滚,验证结果返回为true,说明分布式事务回滚了。

到此我们已经会使用PlatformTransactionManager和TransactionTemplate进行简单事务处理了,那如何应用到实际项目中去呢?接下来让我们看下如何在实际项目中应用Spring管理事务。

 

接下来看一下如何将Spring管理事务应用到实际项目中,为简化演示,此处只定义最简单的模型对象和不完整的Dao层接口和Service层接口:

 

1、 首先定义项目中的模型对象,本示例使用用户模型和用户地址模型:

 

模型对象一般放在项目中的model包里。

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.model;  
  2. public class UserModel {  
  3.     private int id;  
  4.     private String name;  
  5.     private AddressModel address;  
  6.     //省略getter和setter  
  7. }  

 

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.model;  
  2. public class AddressModel {  
  3.     private int id;  
  4.     private String province;  
  5.     private String city;  
  6.     privateString street;  
  7.     private int userId;  
  8.     //省略getter和setter  
  9. }  

 

2.1、定义Dao层接口:

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.service;  
  2. import cn.javass.spring.chapter9.model.UserModel;  
  3. public interface IUserService {  
  4.     public void save(UserModel user);  
  5.     public int countAll();  
  6. }  
  7.    

 

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.service;  
  2. import cn.javass.spring.chapter9.model.AddressModel;  
  3. public interface IAddressService {  
  4.     public void save(AddressModel address);  
  5.     public int countAll();  
  6. }  

 

2.2、定义Dao层实现:

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.dao.jdbc;  
  2. //省略import,注意model要引用chapter包里的  
  3. public class UserJdbcDaoImpl extends NamedParameterJdbcDaoSupport implements IUserDao {  
  4.     private final String INSERT_SQL = "insert into user(name) values(:name)";  
  5.     private final String COUNT_ALL_SQL = "select count(*) from user";  
  6.     @Override  
  7.     public void save(UserModel user) {  
  8.         KeyHolder generatedKeyHolder = new GeneratedKeyHolder();  
  9.         SqlParameterSource paramSource = new BeanPropertySqlParameterSource(user);  
  10.         getNamedParameterJdbcTemplate().update(INSERT_SQL, paramSource, generatedKeyHolder);  
  11.         user.setId(generatedKeyHolder.getKey().intValue());  
  12.     }  
  13.     @Override  
  14.     public int countAll() {  
  15.        return getJdbcTemplate().queryForInt(COUNT_ALL_SQL);  
  16.     }  
  17. }  

 

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.dao.jdbc;  
  2. //省略import,注意model要引用chapter包里的  
  3. public class AddressJdbcDaoImpl extends NamedParameterJdbcDaoSupport implements IAddressDao {  
  4.     private final String INSERT_SQL = "insert into address(province, city, street, user_id)" + "values(:province, :city, :street, :userId)";  
  5.     private final String COUNT_ALL_SQL = "select count(*) from address";  
  6.     @Override  
  7.     public void save(AddressModel address) {  
  8.         KeyHolder generatedKeyHolder = new GeneratedKeyHolder();  
  9.         SqlParameterSource paramSource = new BeanPropertySqlParameterSource(address);  
  10.         getNamedParameterJdbcTemplate().update(INSERT_SQL, paramSource, generatedKeyHolder);  
  11.         address.setId(generatedKeyHolder.getKey().intValue());  
  12. }  
  13.     @Override  
  14.     public int countAll() {  
  15.         return getJdbcTemplate().queryForInt(COUNT_ALL_SQL);  
  16.     }  
  17. }  

 

3.1、定义Service层接口,一般使用“I×××Service”命名:

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.service;  
  2. import cn.javass.spring.chapter9.model.UserModel;  
  3. public interface IUserService {  
  4.     public void save(UserModel user);  
  5.     public int countAll();  
  6. }  
  7.   
  8.   
  9. package cn.javass.spring.chapter9.service;  
  10. import cn.javass.spring.chapter9.model.AddressModel;  
  11. public interface IAddressService {  
  12.     public void save(AddressModel address);  
  13.     public int countAll();  
  14. }  

 

3.2、定义Service层实现,一般使用“×××ServiceImpl”或“×××Service”命名:

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.service.impl;  
  2. //省略import,注意model要引用chapter包里的  
  3. public class AddressServiceImpl implements IAddressService {  
  4.     private IAddressDao addressDao;  
  5.     private PlatformTransactionManager txManager;  
  6.     public void setAddressDao(IAddressDao addressDao) {  
  7.         this.addressDao = addressDao;  
  8.     }  
  9.     public void setTxManager(PlatformTransactionManager txManager) {  
  10.         this.txManager = txManager;  
  11.     }  
  12.     @Override  
  13.     public void save(final AddressModel address) {  
  14.         TransactionTemplate transactionTemplate = TransactionTemplateUtils.getDefaultTransactionTemplate(txManager);  
  15.         transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
  16.            @Override  
  17.            protected void doInTransactionWithoutResult(TransactionStatus status) {  
  18.                 addressDao.save(address);  
  19.            }  
  20.         });  
  21.     }  
  22.     @Override  
  23.     public int countAll() {  
  24.         return addressDao.countAll();  
  25.     }  
  26. }  
  27.    

 

 

java代码:
Java代码    收藏代码
  1.       
  2. package cn.javass.spring.chapter9.service.impl;  
  3. //省略import,注意model要引用chapter包里的  
  4. public class UserServiceImpl implements IUserService {  
  5.     private IUserDao userDao;  
  6.     private IAddressService addressService;  
  7.     private PlatformTransactionManager txManager;  
  8.     public void setUserDao(IUserDao userDao) {  
  9.         this.userDao = userDao;  
  10.     }  
  11.     public void setTxManager(PlatformTransactionManager txManager) {  
  12.         this.txManager = txManager;  
  13.     }  
  14.     public void setAddressService(IAddressService addressService) {  
  15.         this.addressService = addressService;  
  16.     }  
  17.     @Override  
  18.     public void save(final UserModel user) {  
  19.         TransactionTemplate transactionTemplate =  
  20.             TransactionTemplateUtils.getDefaultTransactionTemplate(txManager);  
  21.         transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
  22.         @Override  
  23.            protected void doInTransactionWithoutResult(TransactionStatus status) {  
  24.                 userDao.save(user);  
  25.                 user.getAddress().setUserId(user.getId());  
  26.                 addressService.save(user.getAddress());  
  27.            }  
  28.         });  
  29.     }  
  30.     @Override  
  31.     public int countAll() {  
  32.         return userDao.countAll();  
  33.     }  
  34. }  
  35.    
  36.    

 

Service实现中需要Spring事务管理的部分应该使用TransactionTemplate模板类来包装执行。

 

4、定义TransactionTemplateUtils,用于简化获取TransactionTemplate模板类,工具类一般放在util包中:

 

java代码:
Java代码    收藏代码
  1. package cn.javass.spring.chapter9.util;  
  2. //省略import  
  3. public class TransactionTemplateUtils {  
  4.     public static TransactionTemplate getTransactionTemplate(  
  5.             PlatformTransactionManager txManager,  
  6.             int propagationBehavior,  
  7.             int isolationLevel) {  
  8.         
  9.         TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);  
  10.         transactionTemplate.setPropagationBehavior(propagationBehavior);  
  11.         transactionTemplate.setIsolationLevel(isolationLevel);  
  12.         return transactionTemplate;  
  13.     }  
  14.      
  15.     public static TransactionTemplate getDefaultTransactionTemplate(PlatformTransactionManager txManager) {  
  16.         return getTransactionTemplate(  
  17.                 txManager,  
  18.                 TransactionDefinition.PROPAGATION_REQUIRED,  
  19.                 TransactionDefinition.ISOLATION_READ_COMMITTED);  
  20.     }  
  21. }  
  22.    

 

getDefaultTransactionTemplate用于获取传播行为为PROPAGATION_REQUIRED,隔离级别为ISOLATION_READ_COMMITTED的模板类。

 

 

5、数据源配置定义,此处使用第7章的配置文件,即“chapter7/ applicationContext-resources.xml”文件。

 

6、Dao层配置定义(chapter9/dao/applicationContext-jdbc.xml):

 

java代码:
Java代码    收藏代码
  1. <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">    
  2.     <property name="dataSource" ref="dataSource"/>  
  3. </bean>  
  4. <bean id="abstractDao" abstract="true">  
  5.     <property name="dataSource" ref="dataSource"/>  
  6. </bean>    

 

 

java代码:
Java代码    收藏代码
  1. <bean id="userDao" class="cn.javass.spring.chapter9.dao.jdbc.UserJdbcDaoImpl" parent="abstractDao"/>  
  2. <bean id="addressDao" class="cn.javass.spring.chapter9.dao.jdbc.AddressJdbcDaoImpl" parent="abstractDao"/>  

 

 

7、Service层配置定义(chapter9/service/applicationContext-service.xml):

 

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

 

8、准备测试需要的表创建语句,在TransactionTest测试类中添加如下静态变量:

 

java代码:
Java代码    收藏代码
  1. private static final String CREATE_USER_TABLE_SQL =  
  2.     "create table user" +  
  3.     "(id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " +  
  4.     "name varchar(100))";  
  5. private static final String DROP_USER_TABLE_SQL = "drop table user";  
  6.    
  7. private static final String CREATE_ADDRESS_TABLE_SQL =  
  8.     "create table address" +  
  9.     "(id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " +  
  10.     "province varchar(100), city varchar(100), street varchar(100), user_id int)";  
  11. private static final String DROP_ADDRESS_TABLE_SQL = "drop table address";  

 

9、 测试一下吧:

 

java代码:
Java代码    收藏代码
  1. @Test  
  2.     public void testServiceTransaction() {  
  3.         String[] configLocations = new String[] {  
  4.         "classpath:chapter7/applicationContext-resources.xml",  
  5.         "classpath:chapter9/dao/applicationContext-jdbc.xml",  
  6.         "classpath:chapter9/service/applicationContext-service.xml"};  
  7.         ApplicationContext ctx2 = new ClassPathXmlApplicationContext(configLocations);  
  8.          
  9.         DataSource dataSource2 = ctx2.getBean(DataSource.class);  
  10.         JdbcTemplate jdbcTemplate2 = new JdbcTemplate(dataSource2);  
  11.         jdbcTemplate2.update(CREATE_USER_TABLE_SQL);  
  12.         jdbcTemplate2.update(CREATE_ADDRESS_TABLE_SQL);  
  13.          
  14.         IUserService userService = ctx2.getBean("userService", IUserService.class);  
  15.         IAddressService addressService = ctx2.getBean("addressService", IAddressService.class);  
  16.         UserModel user = createDefaultUserModel();  
  17.         userService.save(user);  
  18.         Assert.assertEquals(1, userService.countAll());  
  19.         Assert.assertEquals(1, addressService.countAll());  
  20.         jdbcTemplate2.update(DROP_USER_TABLE_SQL);  
  21.         jdbcTemplate2.update(DROP_ADDRESS_TABLE_SQL);  
  22. }  
  23. private UserModel createDefaultUserModel() {  
  24.     UserModel user = new UserModel();  
  25.     user.setName("test");  
  26.     AddressModel address = new AddressModel();  
  27.     address.setProvince("beijing");  
  28.     address.setCity("beijing");  
  29.     address.setStreet("haidian");  
  30.     user.setAddress(address);  
  31.     return user;  
  32. }  
  33.    

 

从Spring容器中获取Service层对象,调用Service层对象持久化对象,大家有没有注意到Spring事务全部在Service层定义,为什么会在Service层定义,而不是Dao层定义呢?这是因为在服务层可能牵扯到业务逻辑,而每个业务逻辑可能调用多个Dao层方法,为保证这些操作的原子性,必须在Service层定义事务。

 

还有大家有没有注意到如果Service层的事务管理相当令人头疼,而且是侵入式的,有没有办法消除这些冗长的事务管理代码呢?这就需要Spring声明式事务支持,下一节将介绍无侵入式的声明式事务。

 

可能大家对事务定义中的各种属性有点困惑,如传播行为到底干什么用的?接下来将详细讲解一下事务属性。

9.3.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代码:
Java代码    收藏代码
  1. addressDao.save(address);  

 

 

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

 

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

 

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

 

 

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

 

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

 

三、修改测试方法片段:

 

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

 

为如下形式:

 

 

java代码:
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代码:
Java代码    收藏代码
  1. TransactionTemplate transactionTemplate = TransactionTemplateUtils.getDefaultTransactionTemplate(txManager);  

 

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

 

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

 

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

 

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

 

3、修改UserServiceImpl的save方法片段

 

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

 

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

 

 

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

 

4、修改测试方法片段:

 

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

 

为如下形式:

 

 

java代码:
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传播行为

 

       图9-13 Supports+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决定使用配置方式实现事务管理。

 

 

9.3.6  配置方式实现事务管理

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

 

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

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

 

java代码:
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代码:
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代码:
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代码:
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代码:
Java代码    收藏代码
  1. //真实抛出的异常.name.indexOf(配置中指定的需要回滚/不回滚的异常名)  
  2. exceptionClass.getName().indexOf(this.exceptionName)  
  3.    

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

 

 

2.3、定义代理Bean

 

java代码:
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代码:
Java代码    收藏代码
  1. IUserService userService =  
  2. ctx2.getBean("userService", IUserService.class);  
  3. IAddressService addressService =  
  4. ctx2.getBean("addressService", IAddressService.class);  

 

替换为:

 

java代码:
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*">”定义,底层代理会应用相应定义中的事务属性来创建或关闭事务。

 

 

图9-18 代理方式实现事务管理

       如图9-18,代理方式实现事务管理只是将硬编码的事务管理代码转移到代理中去由代理实现,在代理中实现事务管理。

 

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

 

如图9-19所示,在目标对象targetUserService的save方法内调用事务方法“this.otherTransactionMethod()”将不会应用配置的传播行为RequriesNew,开启新事务,而是使用save方法的已开启事务,如果非要这样使用如下方式实现:

 

1、  修改TransactionProxyFactoryBean配置定义,添加exposeProxy属性为true;

2、  在业务方法内通过代理对象调用相应的事务方放,如 “((IUserService)AopContext.currentProxy()).otherTransactionMethod()”即可应用配置的事务属性。

3、  使用这种方式属于侵入式,不推荐使用,除非必要。

 

图9-19 代理方式下的自我调用

 

       配置方式也好麻烦啊,每个业务实现都需要配置一个事务代理,发展到这,Spring想出更好的解决方案,Spring2.0及之后版本提出使用新的“<tx:tags/>”方式配置事务,从而无需为每个业务实现配置一个代理。

 

 

原创内容,转载请注明出处【http://sishuok.com/forum/blogPost/list/2506.html

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/707694
推荐阅读
相关标签
  

闽ICP备14008679号