zl程序教程

您现在的位置是:首页 >  后端

当前栏目

spring学习笔记(22)声明式事务配置,readOnly无效写无异常

2023-09-14 08:57:15 时间
div >在上一节内容中,我们使用了编程式方法来配置事务,这样的优点是我们对每个方法的控制性很强,比如我需要用到什么事务,在什么位置如果出现异常需要回滚等,可以进行非常细粒度的配置。但在实际开发中,我们可能并不需要这样细粒度的配置。另一方面,如果我们的项目很大,service层方法很多,单独为每个方法配置事务也是一件很繁琐的事情。而且也可能会造成大量重复代码的冗杂堆积。面对这些缺点,我们首要想到的就是我们spring中的AOP了。spring声明式事务的实现恰建立在AOP之上。
在这一篇文章中,我们介绍spring的声明式事务配置。


property name="transactionManager" ref="transactionManager" / !-- 指定一个事务管理器-- property name="transactionAttributes" !-- 配置事务属性 `-- props prop key="add*" PROPAGATION_REQUIRED,-Exception /prop prop key="update*" PROPAGATION_REQUIRED,+Exception /prop prop key="delete*" PROPAGATION_REQUIRED /prop prop key="*" PROPAGATION_REQUIRED /prop /props /property /bean bean property name="beanNames" !-- 配置需要代理的Bean-- list value myBaseServiceImpl /value /list /property property name="interceptorNames" !-- 声明拦截器-- list value transactionInterceptor /value /list /property /bean !-- 测试用到的相关依赖-- bean id="myBaseDao" property name="sessionFactory" ref="sessionFactory" / /bean bean id="myBaseServiceImpl" property name="myBaseDao" ref="myBaseDao" / /bean

在实例中我们通过配置拦截器和代理生成器。在配置TransactionInterceptor事务属性时,key对应于方法名,我们以add*来匹配目标类中所有以add开头的方法,在针对目标对象类的方法进行拦截配置事务时,我们根据属性的定义顺序拦截,如果它被key="add*"所在事务属性拦截,即使后面有key="*"可以匹配任意方法,也不会再次被拦截。关于标签内的事务属性格式如下:
传播行为 [,隔离级别] [,只读属性] [,超时属性] [,-Exception] [,+Exception]
其中除了传播行为外,其他都是可选的。每个属性说明可见下表


取值必须以“PROPAGATION_”开头,具体包括:PROPAGATION_MANDATORY、PROPAGATION_NESTED、PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED、PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS,共七种取值。
取值必须以“ISOLATION_”开头,具体包括:ISOLATION_DEFAULT、ISOLATION_READ_COMMITTED、ISOLATION_READ_UNCOMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE,共五种取值。
如果事务是只读的,那么我们可以指定只读属性,使用“readOnly”指定。否则我们不需要设置该属性。
+Exception 即使事务中抛出了这些类型的异常,事务仍然正常提交。必须在每一个异常的名字前面加上“+”。异常的名字可以是类名的一部分。比如“+RuntimeException”、“+tion”等等。可同时指定多个,如+Exception1,+Exception2
-Exception 当事务中抛出这些类型的异常时,事务将回滚。必须在每一个异常的名字前面加上“-”。异常的名字可以是类名的全部或者部分,比如“-RuntimeException”、“-tion”等等。可同时指定多个,如-Exception1,-Exception2

从配置文件中可以看出,我们可以配置多个拦截器和多个Bean来适配不同的事务。这种声明式事务使用起来还是很方便的。


public class MyBaseServiceImpl implements MyBaseService{

 private MyBaseDao myBaseDao;

 @Override

 public void queryUpdateUser(final Integer id,final String newName) {

 User user = myBaseDao.queryUnique(User.class, id);

 System.out.println(user);

 user.setName(newName);

 myBaseDao.update(user);

 System.out.println(user);

 public void setMyBaseDao(MyBaseDao myBaseDao) {

 this.myBaseDao = myBaseDao;

}

可见,我们去除了事务模板的侵入式注入,同时还去除了事务(在每一个方法中的)侵入式配置。当然,编程式事务的好处是能将事务配置细粒度到每个方法当中。。当我们大部分方法的事务还是一致的,我们可以使用声明式事务,针对那些需要独立配置的,我们可以将其排除出声明式事务,然后使用编程式事务或后面我们会提到的注解式事务单独配置。


public void test(){ ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/spring-datasource.xml"); MyBaseServiceImpl myBaseService= (MyBaseServiceImpl) ac.getBean("myBaseServiceImpl"); myBaseService.queryUpdateUser(1, "newName2"); }

运行测试方法,会发现报错:


java.lang.ClassCastException: com.sun.proxy.$Proxy8 cannot be cast to com.yc.service.MyBaseServiceImpl
意思是我们的代理类无法转换成我们自定义的Service实现类。究其原因,是因为我们的BeanNameAutoProxyCreator没有默认使用CGLib代理,这样我们的代理类是利用JDK动态代理基于接口创建的,而非基于类创建,我们有以下两种解决方法:
1. 将代理类转换成MyBaseServiceImpl所实现的接口MyBaseService而非MyBaseServiceImpl:
MyBaseService myBaseService= (MyBaseService) ac.getBean("myBaseServiceImpl");
2. 在BeanNameAutoProxyCreator配置下添加:
property name="proxyTargetClass" value="true"/ ,即


DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtaining JDBC connection
DEBUG: org.hibernate.engine.jdbc.internal.LogicalConnectionImpl - Obtained JDBC connection
DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin
DEBUG: org.hibernate.loader.Loader - Done entity load
User [id=1, name=newName]
User [id=1, name=newName2]
DEBUG: org.springframework.orm.hibernate4.HibernateTransactionManager - Initiating transaction commit
DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - committing
这和我们使用编程式事务的结果基本是一致的。


现在,在我们的拦截器中稍微修改一行:
prop key="*" PROPAGATION_REQUIRED,readOnly /prop
我们将其设置为只读模式,这时候,调用我们的测试方法,queryUpdateUser(1,”newName3”)(因为前面测试已将name修改成newName2,为了显示不同的结果,这里射程newName3做参数)。显然,前面的add*,update*,delete*都不能匹配。这时候必定启动key="*"所属事务。运行方法,我们会发现结果:


User [id=1, name=newName3]
这似乎和我们没设置readOnly应有的结果一致,但我们再次运行,程序没有抛出异常,而且会发现结果仍是:
User [id=1, name=newName2]
User [id=1, name=newName3]
说明我们的修改实际上并没有生效!这时在看DEBUG信息,发现在:
DEBUG: org.hibernate.engine.transaction.spi.AbstractTransactionImpl - begin信息上面多了一行:
DEBUG: org.springframework.jdbc.datasource.DataSourceUtils - Setting JDBC Connection [jdbc:mysql://localhost:3306/yc, UserName=yc@localhost, MySQL Connector Java] read-only
说明当前事务确实为只读模式


这里单独拿出readOnly来分析,主要是针对实际开发中可能遇到的麻烦。设想我们哪天只读属性配置错了。但我们没发现,而当我们试图进行相应的写数据操作时,发现程序并没有出现异常,但数据无论怎么都写不进去。这个时候就要好好看看我们的只读属性有没有跑到它不该到的地方去了!


Spring Boot + WebSocket 实时监控异常 此异常非彼异常,标题所说的异常是业务上的异常。 最近做了一个需求,消防的设备巡检,如果巡检发现异常,通过手机端提交,后台的实时监控页面实时获取到该设备的信息及位置,然后安排员工去处理。 因为需要服务端主动向客户端发送消息,所以很容易的就想到了用WebSocket来实现这一功能。
Spring Boot 系列(八)@ControllerAdvice 拦截异常并统一处理 在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。
阿里特邀专家徐雷Java Spring Boot开发实战系列课程(第18讲):制作Java Docker镜像与推送到DockerHub和阿里云Docker仓库 立即下载