一、前言
我们在做切面编程时,可能会使用到的环绕通知@Around。示例代码如下:
@Aspect
@Component
public class MyAspect {
@Around("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retValue = null;
System.out.println("我是环绕通知之前AAA");
retValue = proceedingJoinPoint.proceed();
System.out.println("我是环绕通知之后BBB");
return retValue;
}
}
使用环绕通知时,我们需要考虑几个问题:
- 假设方法执行前的切面代码出现了异常,会导致主业务方法不执行,这种情况如何处理?
- 假设方法执行后的切面代码出现了异常,可能会导致主业务方法事务回滚,这种情况怎么处理?
- 现要求,如果是切面代码抛出的异常,都直接捕获catch后打印不抛出;而主业务代码出现异常需要抛出去,由系统的统一异常进行处理;现问题是,如何区分切面异常和主业务异常?
- 在满足上述的情况下,又如何确保主业务方法只执行一次?
上述情况,是本人工作中遇到的,换做你,你又该如何处理呢。。。下面我说一下我的解决方案
二、场景复现
@Aspect
@Component
public class MyAspect {
@Pointcut("@annotation(com.junjie.aop.AnnotationLog)")
public void aspect(){
}
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = handler(joinPoint);
return result;
}
//业务很复杂的方法,这里省略...
private Object handler(ProceedingJoinPoint joinPoint) throws Throwable {
/*
方法执行前,这里省略一大堆前面代码
含有一段异常代码:
int i = 10/0;
*/
Object result = joinPoint.proceed();
/*
方法执行后,这里省略一大堆前面代码
含有一段异常代码:
int i = 10/0;
*/
//放行主业务方法
return result;
}
}
这个切面若是执行,肯定会出现异常并且影响到了主业务异常的执行。
三、解决方案
3.1 方案一
直接对handler方法进行catch,异常捕获,代码如下:
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = handler(joinPoint);
}catch (Exception e){
//处理掉异常e,不抛出,代码省略
//再执行主方法,进行放行
result = joinPoint.proceed();
}
return result;
}
显然,上述代码,成功的捕获了方法执行前的切面异常,并放行了主业务方法,也达到了,切面代码异常不影响主业务正常执行流程。
假设,我们改写一下handler方法:
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = handler(joinPoint);
}catch (Exception e){
//处理掉异常e,不抛出,代码省略
//再执行主方法,进行放行
result = joinPoint.proceed();
}
return result;
}
//业务很复杂的方法,这里省略...
private Object handler(ProceedingJoinPoint joinPoint) throws Throwable {
/*
方法执行前,这里省略一大堆前面代码
此处的代码全部正常,不会抛异常了
*/
Object result = joinPoint.proceed();
/*
方法执行后,这里省略一大堆前面代码
含有一段异常代码:
int i = 10/0;
*/
//放行主业务方法
return result;
}
分析一下代码,我们发现此方案存在以下缺陷:
- 若handler中执行joinPoint.proceed();时出现异常,即主业务异常,也被我们的catch住了,并做了处理,紧接着又再执行一次joinPoint.proceed();放行。这里不仅catch了主业务逻辑异常,而且还执行了两次主业务方法。
- 若是方法执行前不再抛出异常了,而方法执行后会抛出异常,这时会执行catch里头的代码,joinPoint.proceed();被执行了两次。
3.2 方案二
思路:
引入一个代理类,代理执行主业务方法,并在这个代理类做一些限制处理,代码如下:
切面代码:
@Aspect
@Component
public class MyAspect {
@Pointcut("@annotation(com.junjie.aop.AnnotationLog)")
public void aspect(){
}
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
JoinPointProxy pointProxy = new JoinPointProxy(joinPoint);
Object result = null;
try {
result = handler(pointProxy);
}catch (Exception e){
//主业务逻辑异常,直接抛出,不捕获不处理
if (pointProxy.isExecuted()){
Exception exception = pointProxy.getException();
if (exception != null){
throw exception;
}
}
//切面代码异常,捕获并进行处理
LoggerUtils.error("切面异常",e);
//确保主业务方法已执行,并放行
if (pointProxy.isExecuted()){
return pointProxy.getResult();
}else {
return pointProxy.proceed();
}
}
return result;
}
//业务很复杂的方法,这里省略...
private Object handler(JoinPointProxy pointProxy) throws Throwable {
/*
方法执行前,这里省略一大堆前面代码
此处的代码全部正常,不会抛异常了
*/
Object result = pointProxy.proceed();
/*
方法执行后,这里省略一大堆前面代码
含有一段异常代码:
int i = 10/0;
*/
//放行主业务方法
return result;
}
}
代理类:
public class JoinPointProxy {
//方法是否已执行
private boolean executed;
//方法执行结果
private Object result;
//方法执行异常
private Exception exception;
//连接点
private ProceedingJoinPoint joinPoint;
public JoinPointProxy(ProceedingJoinPoint joinPoint) {
this.joinPoint = joinPoint;
}
public Object proceed() throws Throwable {
if (!isExecuted()){
try {
result = joinPoint.proceed();
}catch (Exception e){
exception = e;
throw e;
}finally {
executed = true;
}
}
return result;
}
public boolean isExecuted() {
return executed;
}
public Object getResult() {
return result;
}
public Exception getException() {
return exception;
}
}