Spring AOP使用注意点

Spring中的AOP面向切面编程结合IOC容器,是编程的灵活性大大提高。结合支持通配符和注解等等一系列方便切入的特性,真香!我这里分享几个需要注意的地方,以及以我的经验总结几条原则。

Spring AOP简介

Spring AOP 基于动态代理,为以下两种:

  • 基于接口,使用JDK Dynamic Proxy
  • 非接口,使用CGLIB自动生成子类实现代理

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{
     

}

因为是切入式一环套一环的,先执行切入的必然最后切出。
Spring AOP使用注意点_第1张图片
事务切面(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切入点语法详解

你可能感兴趣的:(spring,aop)