我觉得学习是一个反复的过程,隔了一段时间再回过头来看之前学的东西,会有新的感悟。
在以前学习动态代理的时候,虽然明白了如何实现动态代理,但是会觉得很麻烦,因为为了实现动态代理,我们需要实现InvocationHandler接口,通过在invoke方法中去增加无关业务的代码,另外还需要去使用Proxy类构造一个代理类。会觉得这样用起来更加麻烦,倒还不如直接重写类来得方便。
也了解过aop,看过一些文章,说通过aop我们可以把一些无关业务的代码横向切入到原有的方法中,许多文章以日志的记录为例子来介绍。可是我却觉得类似这样的操作并不能说服自己aop是真的有用的:
//在代理真实对象前我们可以添加一些自己的操作
System.out.println("before rent house");
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
method.invoke(subject, args);
//在代理真实对象后我们也可以添加一些自己的操作
System.out.println("after rent house");
上个月,在项目中总算是把aop给用上了,也在使用的过程中确切的感受到了使用aop的好处。
在这个模块中,主要是封装请求参数,再去调第三方的接口,然后返回数据给前端。在这个过程中,当出现了各种异常,需要将异常反馈到前端页面,并且对于一些意料之外的异常,需要通过钉钉机器人将错误信息报到相关的钉钉群中。
在以前的模块中,我是通过在controller层使用try catch来进行处理,在catch到异常的时候,将错误信息组装好,然后调用钉钉发送通知工具方法。如此,来实现异常通知。
我写接口的时候,习惯把service层、dao层都给写好,并且通过单元测试,最后再来实现controller层。而在实现controller层的时候,我发现我不得不在每个方法都去写try catch,重复率非常高。于是想到了aop,于是改装了一下service层的方法,使其抛出的全部转化成一个实现了RuntimeException的Exception。这样,controller层就无需显示的使用try catch去处理异常,然后通过环绕通知,来实现异常时的catch。
这样,controller层的代码就会简化很多,去掉了重复的try catch操作。后面我又顺便把controller层返回数据的格式统一了,统一返回一个ResultBean,通过statusCode来判断接口调用是否出现异常,当statusCode不为0000即接口出现异常的时候,会把异常信息封装到errMsg中。也正是因为使用了aop,可以统一处理,因此才改得这么快。
上面是aop的使用。
而最近在测试一个service层方法的时候,遇到了一个问题:预期的事务回滚没有发生。一开始很懵逼,后面理解了spring的事务是跟aop有关的,就好理解很多了。
场景是这样的,在一个定时器类中,我写了一个定时器方法,在这个定时器方法中,会去调用同一个类中的业务逻辑方法,像这样:
@Scheduled(cron = "0 0 19 * * ?") //每天下午7点钟执行一次
public void gogogo(){
try {
core();
}catch (Exception e){
logger.error("出现异常,回滚。");
SendMessageUtils.sendTextMessage("7774","获取圈存账单出现异常:" + ExceptionRecordUtils.getExceptionDetailInfo(e));
}
}
@Transactional(rollbackFor = Exception.class)
public void core(){
//省略了一些代码
QueryStoreRecordInput input = new QueryStoreRecordInput();
List<StoreRecordBean> beanList = storeRecordService.queryStoreRecord(input);
//插入数据库
for (StoreRecordBean bean : beanList){
int result = storeDao.insertStoreRecords(bean);
logger.info("********【获取圈存账单】入库结果:" + (result==1?"success":"fail"));
}
}
}
在单元测试的时候,我故意让主键重复,结果竟然没有回滚。主键重复前的记录都成功插入了。更让我觉得奇怪的时候,在单元测试中直接调用gogogo方法出现异常时不会回滚,而单独调用core方法时,却会回滚。
于是上网找了一些文章来看,spring事务、aop、动态代理等知识串在一起,才算是想懂了为什么会出现这些奇怪的现象。
是这样的,spring事务的原理是动态代理,在开启了事务注解扫描后,在启动项目的时候,spring在实例化bean的时候,在扫描到@Transactional注解的时候,会为该类生成一个代理类,在这个代理类中,对带有@Transactional注解的类进行了改进,增加了开启事务,以及出现异常时自动回滚的代码,如此实现@Transactional事务回滚的功能。
而在上面的代码中,新生成的代理类中,较原本的类而言,多了一个新的core方法,但是原本的core方法依然是存在的。而当我们在单元测试的时候,直接调用gogogo方法,由于在gogogo方法中我们调用的是core()方法,因此实际上调用的还是原本的core方法,即它就是原本的样子,没有开启事务功能的。而当我们在单元测试的时候直接调用core方法,由于我们使用的代理类的实例对象,因此我们调用了是代理类生成的新的core方法,即增加了事务功能的core方法,因此在这个测试中,当出现了异常,在代理类生成的新的core方法中的catch中,会通过rollback方法来实现事务回滚。
明白这个原理后,就知道要怎么改进了:
@Transactional(rollbackFor = Exception.class)
@Scheduled(cron = "0 0 19 * * ?") //每天下午7点钟执行一次
public void gogogo(){
try {
core();
}catch (Exception e){
logger.error("出现异常,回滚。");
SendMessageUtils.sendTextMessage("7774","获取圈存账单出现异常:" + ExceptionRecordUtils.getExceptionDetailInfo(e));
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
public void core(){
//省略了一些代码
QueryStoreRecordInput input = new QueryStoreRecordInput();
List<StoreRecordBean> beanList = storeRecordService.queryStoreRecord(input);
//插入数据库
for (StoreRecordBean bean : beanList){
int result = storeDao.insertStoreRecords(bean);
logger.info("********【获取圈存账单】入库结果:" + (result==1?"success":"fail"));
}
}
}
我们把@Transactional注解放到gogogo方法中,当我们启动项目的时候,spring会为该类生成一个代理类,并且生成了一个新的gogogo方法,这个gogogo方法是被带有事务开启与回滚的try catch包围的gogogo方法。但是由于原本的gogogo方法中,我使用了try catch,因此必须得在原本的catch的时候显示的声明需要回滚,否则就会相当于没有出现异常,就不会出现事务回滚了。
所以说,理解了aop、动态代理,会对spring的事务有更好的理解。
我之前觉得学习动态代理,了解InvocationHandler、Proxy等类来实现动态代理,只能写个demo来玩玩,没什么意思。但后面发现,当把知识贯穿起来的时候,才发现学习基础知识是多么的有必要。spring的事务管理,就是利用的动态代理。我们直接把bean交由spring管理,spring根据注解去生成新的代理类,这些对我们都是透明的,我们都是直接拿来用来。
以上,是对aop的一些学习与使用的感悟~