一、背景介绍
由于程序中大量方法需要监控执行耗时,因此写了基于注解的Aop类来减少重复代码,主要作用是通过环绕通知在方法执行前后进行耗时计算,最后输出到日志/监控。
相关代码如下:
// 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface AddExecTime{
String processName();
}
/**
* 用于记录各过程耗时ms的Aop类
*/
@Slf4j
@Aspect
@Component
public class CostTimeAop {
@Pointcut(value = "@annotation(com.demo.web.annotation.AddExecTime)")
public void addTimelineMsg(){
}
@Around(value = "addTimelineMsg()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
AddExecTime addExecTime = method.getAnnotation(AddExecTime.class);
StopWatch stopWatch = new StopWatch();
Object result = null;
try {
log.info("Process {} ,add timeline msg",addExecTime.processName());
stopWatch.start();
result = joinPoint.proceed();
}catch (Exception e){
log.error("CostTimeAop around error",e);
}finally {
stopWatch.stop();
log.info("{}方法调用完成,耗时{}ms",addExecTimeToQTrace.processName(),stopWatch.getTime());
return result;
}
}
}
二、问题描述
预期效果是从Controller层开始添加方法,然后对其调用的核心方法加上耗时监控注解,每个方法最后都应该输出相关耗时到日志/监控。
然而,在实际调用过程中却发现,只有Controller层注解输出了耗时及Service层第一个注解输出了耗时。这是为什么???
相关代码如下:
//Controller层
@RestController
public WebController{
@Autowired
private WebService webservice;
@AddExecTime(processName = "Controller层getResult方法")
public String getResult(){
return webservice.a();
}
}
// Service层
@Service
public WebService{
@AddExecTime(processName = "Service层a方法")
public String a(){
return webservice.b();
}
@AddExecTime(processName = "Service层b方法")
public String b(){
return webservice.c();
}
@AddExecTime(processName = "Service层c方法")
public String c(){
return "I am free!";
}
}
三、查找原因
首先,AspectJ切面的原理是对目标类生成一个代理对象,然后对代理对象进行前置/后置/环绕操作,拿WebService做例子。
clase WebServiceProxy {
private WebService webService;
...
public String a(){
doBefore();
webService.b();
doAfter();
}
public String b() {
doBefore();
webService.c();
doAfter();
}
public String c() {
doBefore();
// 执行c方法逻辑
doAfter();
}
public doBefore() {
//前置通知逻辑
}
public doAfter() {
//后置通知逻辑
}
}
乍一看貌似没问题,但是要注意的是,代理类使用的是目标类的方法,即WebService的方法,而非WebServiceProxy。因此代理类调用a方法时,其a方法接下来调用的是webService中不带前置/后置通知的b方法,之后又同理。
所以,问题总结为同一个类出现切面注解嵌套时,并不总是由代理类调用代理方法,导致切面注解嵌套后只有第一个被调用的方法能够执行前/后置通知。
四、解决方法
每个方法在调用下一层方法时,都调用其代理类的代理方法去执行。需要注意的是,这种做法如果获取不到代理类就会导致方法调用失败,因此需要使得代理对象能够被获取。
那么如何使用代理对象可获得?
// 如果使用的xml方式,则spring.xml中添加expose-proxy="true"
<aop:aspectj-autoproxy expose-proxy="true"/>
// 如果使用注解方式,则在切面类加上expose-proxy属性
@EnableAspectJAutoProxy(exposeProxy = true)
接下里修改实际调用方法如下使用WebService类进行演示:
@Service
public WebService{
@AddExecTime(processName = "Service层a方法")
public String a(){
return ((WebService)AopContext.currentProxy()).b();
}
@AddExecTime(processName = "Service层b方法")
public String b(){
return ((WebService)AopContext.currentProxy()).c();
}
@AddExecTime(processName = "Service层c方法")
public String c(){
return "I am free!";
}
}
参考资料:
https://www.cnblogs.com/mzcx/p/11430846.html
https://www.cnblogs.com/chihirotan/p/7356683.html
https://blog.csdn.net/u013212754/article/details/103138486
https://www.jianshu.com/p/1e2f9168a6c7