Spring中的AOP面向切面编程结合IOC容器,是编程的灵活性大大提高。结合支持通配符和注解等等一系列方便切入的特性,真香!我这里分享几个需要注意的地方,以及以我的经验总结几条原则。
Spring AOP 基于动态代理,为以下两种:
AOP作为一种编程模型,还有着很多实现。基本需要结合代码自动生成和动态编译一些手段来使用。在JAVA语言编程中,有很多字节码生成工具除了cglib,还有 Javassist 、ASM 、Byte Buddy等等。参见:关于字节码生成代理的性能比较。
Spring AOP的很多理念使用了AspectJ的,包括切入点表达式、相关注解(比如@Aspect
,@Around
,@Pointcut
)等等。AspectJ提供加载时编织(Load-Time Weaving)和编译时编织(Compile-Time Weaving),功能强大但是需要配置较多,特别是编译织入需使用AspectJ Compiler。所以Spring的AOP选择了更为简单易用的方案。
顺便提一下静态代理,手动编码实现设计模式中的代理模式。很多文章也把AspectJ在编译器生成代理类,称为静态代理。理解就是不在运行时动态生成,一次生成就运行就直接使用了。
子包路径通配用法示例:
@Aspect
@Component
public DemoAdvice {
// 所有service层方法
@Pointcut("execution(public * cn.x.xx.demo..service..*(..))")
public void servicePoint() {
}
// 所有dao层方法
@Pointcut("execution(public * cn.x.xx.demo..dao..*(..))")
public void daoPoint() {
}
// 所有被注解@LogTime的方法
@Pointcut("@annotation(LogTime)")
public void LogTimePoint() {
}
// 组合以上所有切入点
@Pointcut("servicePoint()||daoPoint()||LogTimePoint()")
public void allPoints() {
}
@Around("allPoints()")
public Object logCall(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
//打印切入点方法全称
log.info("Call method:{}", methodName);
return pjp.proceed();
}
}
通过Order来指定顺序,整数,值越小优先级越高,优先级高的先执行。
@Aspect
@Order(10)
@Component
public class LogTimeAop{
}
因为是切入式一环套一环的,先执行切入的必然最后切出。
事务切面(Transaction Aspect)默认优先级为最低值
LOWEST_PRECEDENCE = Integer.MAX_VALUE
无法直观解读的返回值,因为切入后操作可能会更改返回值。
@Service
public class DemoServiceImple implements DemoService {
@AopPointFlag
Result handle(DemoParam param) {
// 从这里看是不会返回null的
return new Result();
}
}
public class DemoAdivce {
@Pointcut("@annotation(AopPointFlag)")
public void point() {
}
@Around("point()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 在某些情况返回null,比如参数验证不正确
if(someCondition(proceedingJoinPoint.getArgs())) {
return null;
}
return pjp.proceed();
}
}
在切入处理中改变返回值可能超出接口定义描述,变得隐晦和不确定,出现问题也不容易排查。
// 这里的result 可能会返回null,并且很隐蔽
Result result = DemoService.handle(DemoParam param);
doSomething(result);
如果在切入中处理时需要抛出异常,那么这个异常栈会因为增加了动态代理的类及包装工具方法,方法栈的层数显著增多了,导致异常堆栈也加深了。
未切入时的抛出异常的堆栈:
cn.x.xx.demo.ServiceException: test ex.
at cn.x.xx.demo.service.impl.DemoServiceImpl.test(DemoServiceImpl.java:18)
at cn.x.xx.demo.DemoApplication.main(DemoApplication.java:43)
切入一次的抛出异常时的堆栈:
cn.x.xx.demo.ServiceException: test ex.
at cn.x.xx.demo.service.impl.DemoServiceImpl.test(DemoServiceImpl.java:18)
at cn.x.xx.demo.service.impl.DemoServiceImpl$$FastClassBySpringCGLIB$$dc14dbe6.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
at cn.x.xx.demoAdivce.around(DemoAdivce.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at cn.x.xx.demo.service.impl.DemoServiceImpl$$EnhancerBySpringCGLIB$$7b01f47a.test()
at cn.x.xx.demo.DemoApplication.main(DemoApplication.java:43)
如果在一个点上多次切入,这个栈的深度也会翻倍增长。
如果在切入的处理方法中抛出异常,会是如下异常堆栈。
cn.x.xx.demo.common.ServiceException: test
at cn.x.xx.demo.DemoAdivce.around(DemoAdivce.java:22)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at cn.x.xx.demo.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$8f8ce8b6.test()
at cn.x.xx.demo.DemoApplication.main(DemoApplication.java:43)
如果,切入多次,又在某个切入的处理中抛出了异常,那么真的是很酸爽。找瞎氪金狗眼的节奏,这就是为什么我要花时间写这篇文章的原因。希望大家堆代码时,一定要注意保护自己和同事们的视力。
那么在明白了一些陷阱和经历了一些痛苦后,我就目前认知对Spring AOP使用的基本原则作一个小节。
勿滥用AOP
有汇合点的地方可以从设计上避免使用AOP
同一切入点上多次切入要考虑合并
AOP的精髓在于那散落的逻辑,选择聚合出共有的一个面,插入我们的处理逻辑。一般这样的逻辑应通用或者是需要统一的处理。比如数据库的事务,使用的地方很分散,处理的逻辑又很一致,提交或者遇到异常回滚。这个时候AOP就显出威力了。
我发现很多时候,使用AOP要达到的目的,是可以被简单的设计和实现所替换的。AOP只是看起编写很简单而已。比如说,明明我们实现的逻辑是有汇入点,比如MVC的Controller层,dispatch就是汇入点,是请求的入口,然后再分发给不同的Controller的方法。并且无论是Servlet的filter还是MVC提供的拦截器都已经有切入执行的扩展点了。如果还是要用AOP把Controller层也切了,就需要认真思考一下,这样做是否必要。
基本来说就一句话,能不用就不用,能少用就少用,用的时候一定要谨慎。
参考:
字节码增强技术-Byte Buddy
Spring AOP之坑:完全搞清楚advice的执行顺序
Spring AOP Reference
AspectJ Quick Reference
Spring 之AOP AspectJ切入点语法详解