简述

面向切面(方法)编程(Aspect Oriented Programming)就是在不改动原方法的基础上,为原方法添加新的功能。在需要为一批方法添加一段重复代码时,如日志记录,使用AOP思想可以大大减少重复代码量,提高开发效率,并且易于维护。而Spring事务管理,底层实现也是通过AOP来完成的。

QuickStart

我们先引入一段Demo,来看看Spring AOP的实际使用方法。

使用Spring框架中提供的AOP实现,需要先引入依赖。

<!--Spring Aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Spring Aop END-->

接着我们编写一个AOP类,我们想记录方法开始执行的时间和结束执行的时间。

@Component
@Aspect //声明是一个切面类
@Slf4j
public class RecordTimeAspect {

    @Around("execution(* com.itheima.service.DeptService.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        
        //执行原方法 - 业务方法
        Object result = pjp.proceed();

        long end = System.currentTimeMillis();

        log.info("方法执行时间: {} 毫秒",end-begin);

        return result;
    }
}

正常调用方法后,能够在控制台看到输出

[http-nio-8080-exec-3] INFO  com.itheima.aop.RecordTimeAspect-方法执行时间: 9 毫秒

这就是一个简单的AOP用例。

我们可以看到,面向切面编程具有代码无侵入、减少重复代码、提高开发效率、易于维护的特点。

Spring AOP核心概念

  • 为确保对AOP有精准的理解,必须对下列核心术语进行明确界定:

    核心概念 英文 解读 示例
    目标对象 Target 指被AOP增强逻辑所应用的原始对象实例,其内部封装了系统的核心业务功能。 DeptServiceImpl类的实例对象。
    代理对象 Proxy 由AOP框架为目标对象所生成的、遵循代理设计模式的包装对象。客户端的交互实质上是与此代理对象进行,由代理对象负责截获方法调用,并依据切面配置决定增强逻辑的执行时机与目标方法的最终调用。 Spring框架为DeptServiceImpl实例所创建的代理对象。
    连接点 JoinPoint 程序执行流程中一个可被识别的、明确的执行点。在Spring AOP的语境下,连接点通常指代方法的执行这一特定事件。 DeptService接口内任一方法的执行过程。
    切入点 Pointcut 一个用于甄别并匹配连接点的谓词表达式。它精确定义了增强逻辑(即通知)应当作用于何种方法的执行之上。 execution(* com.itheima.service.DeptService.*(..))
    通知 Advice 在由切入点所指定的特定连接点上被执行的附加逻辑,即被模块化的横切关注点代码。通知定义了“执行何种操作”及“于何时执行”。 recordTime方法体内部的性能计时逻辑。
    切面 Aspect 通知(Advice)与切入点(Pointcut)的结构化结合体。一个切面完整地描述了在何处(Pointcut)、何时(Advice类型)执行何种操作(Advice内容)。在实现层面,一个被@Aspect注解的Java类即构成一个切面。 RecordTimeAspect 类。
    织入 Weaving 将切面所定义的增强逻辑集成至目标对象,并最终生成代理对象的构造过程。 Spring容器在初始化阶段,解析@Aspect配置并为匹配的目标对象创建代理实例的过程。

    从根本上讲,AOP的实现机制根植于代理(Proxy)设计模式,对此点的认知是理解其工作原理的基础。

实现原理

Spring AOP的实现架构完全依赖于动态代理(Dynamic Proxy)机制。在容器启动过程中,Spring框架会扫描切面配置,并为所有匹配的目标对象生成代理实例。此代理实例在功能上对原始对象进行了增强。

代理策略的选择取决于目标对象的继承体系结构:

  1. JDK动态代理:若目标对象实现了一个或多个接口,Spring框架将默认采用Java开发工具包(JDK)内建的动态代理机制。此机制会生成一个实现了与目标对象相同接口集的代理类。
  2. CGLIB代理:反之,若目标对象并未实现任何接口,Spring框架将转而使用CGLIB(Code Generation Library)库来创建代理。CGLIB通过动态生成目标类的子类来实现代理功能。

一项重要的技术限制是,基于继承的CGLIB代理机制无法为被final关键字修饰的类或方法生成代理,此为该技术的固有局限。

通知类型&通知顺序

通知类型

通知分为几种类型,分别对应着不同的通知方法执行时间。

注解 描述
@Around 环绕通知,此注解标注的通知方法在目标方法前、后都被执行,如果执行中原方法有异常,后续代码不会继续执行
@Before 前置通知,此注解标注的通知方法在目标方法前被执行
@After 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing 异常后通知,此注解标注的通知方法发生异常后执行

其中@Around环绕通知需要在方法体中自行调用ProceedingJoinPoint.proceed()来让原方法执行,并且返回值必须指定为Object,否则无法接收到返回值,其他通知则不需要。

切入点表达式抽取

如果在每一个注解里面指定切入点表达式,会显得比较繁琐,所以我们可以将重复的切入点表达式从中抽取出来,通过@PointCut注解。

//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint){
    log.info("before ...");
}
通知顺序
  • 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的

  • 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序,前置通知:数字越小先执行; 后置通知:数字越小越后执行

切入点表达式

切入点表达式顾名思义,用于匹配哪些方法要作为切入点加入通知。

有两种形式

  • execution(……):根据方法的签名来匹配

    execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)
    @Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

    其中,?代表此项可以省略,*代表单个任意,..代表任意个任意,也可以用&&,||,!来组装复杂的切入点表达式。注意,参数类型如果是包装类或者对象,需要写全类名,包名类名也不建议省略以提升性能。

    描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。

  • @annotation(……) :根据注解匹配

    • 定义注解

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface LogOperation{
      }
    • 在业务类上加上此注解

      @Override
      @LogOperation
      public List<Dept> list() {
          List<Dept> deptList = deptMapper.list();
          return deptList;
      }
    • 切面方法

      @Slf4j
      @Component
      @Aspect
      public class MyAspect {
          //前置通知
          @Before("@annotation(com.itheima.anno.LogOperation)")
          public void before(){
              log.info("MyAspect6 -> before ...");
          }
      
      }

AOP通知获取方法参数

  • 获取方法入参

    可以通过JoinPoint/ProceedingJoinPoint对象中的getArgs方法获得一个Object[]数组,其中包含了所有入参。

    ProceedingJoinPoint中的proceed(Object[] args)方法可以传入一个Object[]数组,我们可以在AOP通知中对入参进行修改,再传入原方法中。

  • 获取方法返回值

    AroundAfterReturning可以获取到方法的返回值。

    Around方法中,proceedingJoinPoint.proceed()方法的返回值就是原方法的返回值

    AfterReturning则需要在方法形参和注解中声明用来接收返回值的形参

    @AfterReturning(value = "pt()",returning = "ret")
    public void doSometing(Object[] ret){
        //dosometing...
    }

    注意!当方法形参中同时包含多个变量时,Joinpoint对象必须放在第一个形参,不然会报错。

  • 获取抛出的异常

    Around可以用try/catch包围住proceed方法来捕获抛出的异常。

    AfterThrowing和上面获取返回值的形似,需要在方法形参和注解中声明要接收返回值的型参。

    @AfterThrowing(value = "pt()" , throwing = "t"){
        public void doSometing(Throwable t){
            //dosometing...
        }
    }