Spring 的两大核心之一:Spring AOP 详解

1. 什么是AOP

AOP (Aspect Oriented Programming)面向切面编程,是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。AOP是⼀种思想,是对某⼀类事情的集中处理,它的实现方法有很多, Spring AOP、AspectJ、CGLIB等。
在传统的面向对象编程中,程序的功能逻辑被分散在各个对象中,而横切关注点则分散在多个对象之间,导致代码重复、可维护性差,并且难以修改和扩展。AOP 的目标就是解决这些问题。
Spring AOP 是Spring框架提供的一种面向切面编程的技术。它通过引入横切关注点(例如日志记录、事务管理、安全控制等),将其从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。
切面(Aspect)是用来描述横切关注点,是对横切关注点的封装,切面定义了何时、在何处、如何应用横切关注点。在 AOP 中,切面可以横跨多个对象,独立于主业务逻辑。

2. Spring AOP 的核心概念

引⼊AOP依赖
在pom.xml⽂件中添加配置

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

image.png
我们以上述 记录Controller中每个方法的执行时间 这个程序来介绍下面的概念。

2.1. 切点(Pointcut)

用于定义在哪些连接点上应用通知(切面对哪些方法生效)。切点通过表达式进行定义,用于匹配目标对象的一组方法,如匹配所有 public 方法或匹配某个包下的所有方法等,在这些方法执行时切面会被触发。
上面的表达式 execution(* com.bite.demo.controller..(…)) 就是切点表达式。

2.2. 连接点(Joinpoint)

满足切点表达式规则的方法就是连接点,或者说程序中能够被切面插入的点就是连接点,典型的连接点包括方法调用、异常抛出等等。
比如上述例子中所有 com.bite.demo.controller 路径下的方法, 都是连接点。

2.3. 通知(Advice)

切面在特定切点上执行的代码,包括在连接点之前、之后或周围执行的行为。
比如上述程序中记录业务方法的消耗时间, 就是通知。

2.4. 切面(Aspect)

切面(Aspect) = 切点(Pointcut) + 通知(Advice)
切面是横切关注点的模块化单元,它将通知和切点组合在一起,描述了在何处、何时和如何应用横切关注点。
切面所在的类, 我们⼀般称为切面类(被@Aspect注解标识的类)。

2.5. 织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程,可以在编译时、加载时或运行时进行。在 Spring AOP 中,织入发生在运行时,通过代理对象的方式实现。

2.6. 通知类型

  • @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行
  • @Before:前置通知, 此注解标注的通知方法在目标方法前被执行
  • @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行
  • @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行
  • @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行
@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Before("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public void doBefore() {
        log.info("执行AspectDemo.before方法...");
    }

    @After("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public void doAfter() {
        log.info("执行AspectDemo.after方法...");
    }

    @AfterReturning("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public void doAfterReturn() {
        log.info("执行AspectDemo.afterReturn方法...");
    }

    @AfterThrowing("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public void doAfterThrow() {
        log.info("执行AspectDemo.afterThrow方法...");
    }

    @Around("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        log.info("执行AspectDemo.around  目标方法前...");
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.info(joinPoint.toShortString() + "发生异常:e",e);
//            throw new RuntimeException(e);
        }
        log.info("执行AspectDemo.around  目标方法后...");
        return result;
    }
}

测试类代码:

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法");
        return "t1";
    }

    @RequestMapping("/t2")
    public String t2() {
        log.info("执行t2方法");
        int a = 10/0;
        return "t2";
    }
}

当before、after方法和around方法同时存在时,执行顺序:
image.png
当正常响应时,执行顺序:
image.png
当发生异常时,执行顺序:
image.png
注意:@AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了。@Around 环绕通知中原始⽅法调⽤时有异常,通知中的环绕后的代码逻辑也不会在执⾏了。

2.7. @Pointcut

上面代码存在⼀个问题, 就是存在⼤量重复的切点表达式 execution(* com.xiaoyu.aop.controller..(…)) , Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要用到时引用该切⼊点表达式即可。
上述代码就可以修改为:
image.png
当其他类使用这个公共切点时,需要写全限定类名:
image.png
执行结果:
image.png

2.8. 切面优先级@Order

当我们在⼀个项⽬中, 定义了多个切⾯类时, 并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法。当⽬标⽅法运⾏的时候, 这些切⾯类中的通知⽅法都会执⾏, 那么这几个通知⽅法的执⾏顺序是什么样的呢?
定义多个切⾯类:

@Slf4j
@Component
@Aspect
public class AspectDemo2 {
    @Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执行 AspectDemo2.doBefore 方法");
    }

    @After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执行 AspectDemo2.doAfter 方法");
    }
}

@Slf4j
@Component
@Aspect
public class AspectDemo3 {
    @Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执行 AspectDemo3.doBefore 方法");
    }

    @After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执行 AspectDemo3.doAfter 方法");
    }
}

@Slf4j
@Component
@Aspect
public class AspectDemo4 {
    @Before("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执行 AspectDemo4.doBefore 方法");
    }

    @After("com.xiaoyu.aop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执行 AspectDemo4.doAfter 方法");
    }
}

运行程序,访问接口:http://127.0.0.1:8080/test/t1
image.png
通过上述程序的运⾏结果, 可以看出:存在多个切面类时, 默认按照切面类的类名字母排序。但这种⽅式不⽅便管理, 我们的类名更多还是具备⼀定含义的。
Spring 给我们提供了⼀个新的注解, 来控制这些切⾯通知的执⾏顺序: @Order
使⽤⽅式如下:
image.png
image.png
image.png
重新运行程序,访问接口,观察运行结果:
image.png
@Order 控制切⾯的优先级,先执⾏优先级较⾼的切⾯, 再执⾏优先级较低的切⾯, 最终执⾏⽬标⽅法。Order的值越大,优先级越低。

3. Spring AOP的实现方式

切点表达式常见有两种表达⽅式:

  1. execution(…):根据⽅法的签名来匹配
  2. @annotation(…) :根据注解匹配

3.1. 基于注解实现AOP(@Aspect注解和execution表达式)

execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:

execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)

其中: 访问修饰符和异常可以省略。
image.png
切点表达式⽀持通配符表达:

  1. *:匹配任意字符,只匹配⼀个元素(返回类型, 包, 类名, ⽅法或者⽅法参数)

     a. 包名使⽤ * 表⽰任意包(⼀层包使⽤⼀个*)
     b.  类名使⽤ * 表⽰任意类
     c.  返回值使⽤ * 表⽰任意返回值类型
     d.  ⽅法名使⽤ * 表⽰任意⽅法
     e. 参数使⽤ * 表⽰⼀个任意类型的参数
    
  2. 两个. : 匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数

     a.  使⽤ .. 配置包名,标识此包以及此包下的所有⼦包
     b.  可以使⽤ .. 配置参数,任意个任意类型的参数
    

切点表达式示例:

  1. TestController 下的 public修饰, 返回类型为String ⽅法名为t1, ⽆参⽅法:

execution(public String com.example.demo.controller.TestController.t1())

  1. 省略访问修饰符

execution(String com.example.demo.controller.TestController.t1())

  1. 匹配所有返回类型

execution(* com.example.demo.controller.TestController.t1())

  1. 匹配TestController 下的所有⽆参⽅法

execution(* com.example.demo.controller.TestController.*())

  1. 匹配TestController 下的所有⽅法

execution(* com.example.demo.controller.TestController.*(…))

  1. 匹配controller包下所有的类的所有⽅法

execution(* com.example.demo.controller..(…))

  1. 匹配所有包下⾯的TestController

execution(* com…TestController.*(…))

  1. 匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法

execution(* com.example.demo…*(…))

@Slf4j
@Component
@Aspect
public class AspectDemo {
    @Before("execution(* com.xiaoyu.aop.controller.*.*(..))")
    public void doBefore() {
        log.info("执行AspectDemo.before方法...");
    }
}

3.2. 基于自定义类和实现AOP(自定义注解和@annotation表达式)

execution表达式更适⽤有规则的, 如果我们要匹配多个⽆规则的⽅法呢, ⽐如:我们想实现一个计录方法执行时间的功能,只作用在TestController中的t1()和UserController中的u1()这两个⽅法上。这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了。
我们可以借助自定义注解的方式以及另⼀种切点表达式 **@annotation **来描述这⼀类的切点。
实现步骤:

  1. 定义一个注解
  2. 实现该注解需要完成的功能(使⽤ @annotation 表达式来描述切点)
  3. 使用该注解(在连接点的方法上添加自定义注解)

自定义注解@TimeRecord:
创建一个注解类

/**
 * 记录方法的执行时间
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeRecord {
}

实现该注解的功能
使用 @annotation 切点表达式定义切点,切面类代码如下:

@Aspect
@Component
@Slf4j
public class AspectDemo5 {
    @Around("@annotation(com.xiaoyu.aop.aspect.TimeRecord)")
    public Object timeRecord(ProceedingJoinPoint joinPoint) {
        long start = System.currentTimeMillis();// 目标方法执行前  记录开始时间
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.info(joinPoint.toString()+"发生异常:e",e);
        }
        log.info(joinPoint.toString()+"cost time:"+(System.currentTimeMillis() - start));
        return result;
    }
}

在连接点的方法上添加自定义注解
在TestController中的t1()和UserController中的u1()这两个⽅法上添加⾃定义注解 @TimeRecord , 其他⽅法不添加。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping("/t1")
    @TimeRecord
    public String t1() {
        log.info("执行t1方法");
        return "t1";
    }

    @RequestMapping("/t2")
    public String t2() {
        log.info("执行t2方法");
        return "t2";
    }
}
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/u1")
    public String u1() {
        log.info("执行u1方法");
        return "u1";
    }

    @RequestMapping("/u2")
    @TimeRecord
    public String u2() {
        log.info("执行u2方法");
        return "u2";
    }
}

运行程序,测试接口:http://127.0.0.1:8080/test/t1
image.png
测试接口:http://127.0.0.1:8080/test/t2
image.png切面通知未执行
测试接口:http://127.0.0.1:8080/user/u1
image.png切面通知未执行
测试接口:http://127.0.0.1:8080/user/u2
image.png

3.3. 基于Spring API 实现AOP(通过xml配置的方式)

3.4. 基于代理实现AOP

本质上,上面三种底层都是基于代理实现AOP的。

4. 使用Spring AOP的优点

使用Spring AOP的主要原因是它可以帮助我们更好地管理各种横切关注点,例如日志记录、事务管理、安全性检查等。

  1. 模块化:Spring AOP将横切关注点从主业务逻辑代码中分离出来,以模块化的方式实现对这些关注点的管理和重用。这样,我们可以更容易地维护代码,并且可以将同一个关注点的逻辑应用到多个方法或类中。
  2. 非侵入式:使用Spring AOP时,我们不需要修改原始业务逻辑代码,只需要在切点和增强中定义我们所需要的逻辑即可。这样,我们可以保持原始代码的简洁性和可读性。
  3. 可重用性:我们可以将同一个切面应用于多个目标对象进行横切处理。这样,我们可以提高代码的重用性,并且可以更加方便地维护和更新切面逻辑。
  4. 松耦合:AOP可以减少各个业务模块之间的耦合度,这是因为我们可以将某些通用的逻辑作为切面来实现,而不是直接在各个业务模块中实现。这样可以使得各个业务模块之间更加独立,从而提高代码的可维护性。

总之,使用Spring AOP可以帮助我们更好地管理和重用横切关注点逻辑,使得代码更易于维护和理解,并且可以提高代码的可重用性和灵活性。

5. Spring AOP实现原理

Spring 是如何实现AOP的?
AOP 实现技术主要分为两大类:静态代理和动态代理。Spring 是基于动态代理来实现AOP的,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 主要基于两种方式实现的: JDK 及 CGLIB 的方式:

  1. JDK 动态代理:对于实现了接口的目标类,Spring AOP 默认使用 JDK 的 java.lang.reflect.Proxy 类来创建代理对象。代理对象会在运行时实现代理接口,并覆盖其中的方法,在方法调用前后执行切面逻辑(即通知,advice)。
  2. CGLIB 动态代理:对于未实现接口的类,Spring AOP 会选择使用 CGLIB 库来生成代理对象。CGLIB 通过字节码技术创建目标类的子类,在子类中重写目标方法并在方法调用前后插入切面逻辑。

执行流程:当客户端通过代理对象调用目标方法时,代理对象会拦截这个调用,根据切面配置找到对应的通知,并按照通知类型的不同执行相应的增强逻辑。例如,如果是环绕通知,它会完全控制原始方法的调用过程,可以在调用前后插入自定义逻辑,甚至决定是否执行原方法。
通过上述方式,Spring AOP 巧妙地实现了对目标对象方法的拦截和增强,从而实现了面向切面编程的功能。
源码解析:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
                             @Nullable Object[] specificInterceptors, TargetSource targetSource) {
    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
        AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)
                                          this.beanFactory, beanName, beanClass);
    }
    //创建代理⼯⼚
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
    /**
      * 检查proxyTargetClass属性值,spring默认为false
      * proxyTargetClass 检查接⼝是否对类代理, ⽽不是对接⼝代理
      * 如果代理对象为类, 设置为true, 使⽤cglib代理
    */
    if (!proxyFactory.isProxyTargetClass()) {
        //是否有设置cglib代理
        if (shouldProxyTargetClass(beanClass, beanName)) {
            //设置proxyTargetClass为true,使⽤cglib代理
            proxyFactory.setProxyTargetClass(true);
        } else {
        /**
          * 如果beanClass实现了接⼝,且接⼝⾄少有⼀个⾃定义⽅法,则使⽤JDK代理
          * 否则CGLIB代理(设置ProxyTargetClass为true )
          * 即使我们配置了proxyTargetClass=false, 经过这⾥的⼀些判断还是可能会将其设为true
        */
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
    proxyFactory.setFrozen(this.freezeProxy);
    if (advisorsPreFiltered()) {
        proxyFactory.setPreFiltered(true);
    }
    // Use original ClassLoader if bean class not locally loaded in overriding
    class loader
    ClassLoader classLoader = getProxyClassLoader();
    if (classLoader instanceof SmartClassLoader && classLoader !=
        beanClass.getClassLoader()) {
        classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
    }
    //从代理⼯⼚中获取代理
    return proxyFactory.getProxy(classLoader);
}

代理工厂有⼀个重要的属性: proxyTargetClass, 默认值为false。也可以通过程序设置:

proxyTargetClass⽬标对象代理⽅式
false实现了接口JDK代理
false未实现接口(只实现了类)CGLIB代理
true实现了接口CGLIB代理
true未实现接口(只实现了类)CGLIB代理

5.1. Spring AOP 使用了哪种代理?

  1. Spring Framework

默认使用JDK动态代理,proxyTargetClass默认为false。如果代理的是接口,且接口至少有⼀个自定义方法,使用JDK动态代理;否则,使用CGLIB代理。如果代理的是没有实现接口的类,使用CGLIB动态代理。

  1. Spring Boot

Spring Boot 2.X 之前的版本,与Spring Framework 保持一致,默认使用JDK动态代理。
Spring Boot 2.X 之后的版本,默认配置使用CGLIB动态代理,proxyTargetClass默认为true。代理的类无论是否实现了接口,都使用CGLIB动态代理,如果需要使用JDK动态代理,可以通过配置项 spring.aop.proxy-target-class=false 来进⾏修改,设置默认为JDK代理。
共同点:底层实现都是JDK和CGLIB。

  • 30
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP(面向切面编程)是Spring框架的一个模块,用于提供横切关注点(Cross-Cutting Concerns)的支持。横切关注点是与应用程序核心业务逻辑无关的功能,例如日志记录、性能统计、事务管理等。 在Spring AOP,通过定义切面(Aspect)来捕获横切关注点,并将其应用到目标对象的方法。切面由切点(Pointcut)和通知(Advice)组成。切点定义了在何处应用通知,通知则定义了在切点处执行的操作。 Spring AOP支持以下几种类型的通知: 1. 前置通知(Before Advice):在目标方法执行之前执行的通知。 2. 后置通知(After Advice):在目标方法执行之后执行的通知,不管方法是否抛出异常。 3. 返回通知(After Returning Advice):在目标方法成功执行并返回结果后执行的通知。 4. 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。 5. 环绕通知(Around Advice):围绕目标方法执行的通知,可以在方法调用前后执行自定义操作。 除了通知,Spring AOP还支持引入(Introduction)和切点表达式(Pointcut Expression)等功能。引入允许为目标对象添加新的接口和实现,而切点表达式则允许开发人员定义切点的匹配规则。 要在Spring应用程序使用AOP,需要进行以下步骤: 1. 引入Spring AOP的依赖。 2. 配置AOP代理。 3. 定义切面和通知。 4. 配置切点和通知之间的关系。 总之,Spring AOP提供了一种便捷的方式来处理横切关注点,使得开发人员可以将关注点与核心业务逻辑分离,提高代码的可维护性和可重用性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值