• 📌 博主简介: 💻 努力学习的 23 级科班生一枚 🚀
  • 🏠 博主主页📎 @灰阳阳
  • 📚 往期回顾Spring拦截器(Intercepter)
  • 每日一言 :如果你认定人的一生只能活一次,那你就没有随波逐流的理由。✅


一、什么是AOP

Aspect Oriented Programming(面向切面编程)是一种编程思想。所谓切面可以理解为特定的一些方法。所以AOP可以看成是面向特定的方法进行编程。比如上一篇文章提到的拦截器,就是AOP的体现。它针对特定的方法进行统一的管理,让代码模块化,易于维护。 包括ExceptionAdvice、ResponseBodyAdvice(统一异常处理、统一数据返回格式)都运用了AOP的思想。

二、SrpingAOP的相关概念

使用Spring框架进行AOP开发一般有两个方式:

  1. 注解实现
  2. XML配置实现

并且需要实现导入SpingAOP依赖:

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

在讲解AOP(面向切面编程)中的这些概念之前,我们先理解一下AOP的目的是什么。在软件开发中,有些功能(比如日志记录、安全检查、事务管理等)会散落在程序的各个模块中,这些功能被称为“横切关注点”。AOP就是为了将这些横切关注点从核心业务逻辑中分离出来,让代码更清晰、更易于维护。

现在,我们来分别解释一下这些核心概念:

  1. 连接点(Join Point)

    • 通俗理解: 程序执行过程中,那些“可以被增强”的特定点。可以想象成你程序运行时的“某个瞬间”或“某个地方”。
    • 详细解释: 连接点是指在应用程序执行过程中,可以插入切面(Aspect)逻辑的任何点。这包括但不限于方法的调用、方法的执行、异常的处理、字段的访问等。 Spring AOP主要支持方法的执行作为连接点。
    • 例子: 当你调用一个 userService.createUser() 方法时,在方法执行前、方法执行后、方法抛出异常时,这些都可以是连接点。
  2. 切点(Pointcut)

    • 通俗理解: 一种“规则”或“条件”,用来精确地选择出你想要增强的那些“连接点”。
    • 详细解释: 切点是一个表达式,它定义了哪些连接点应该被“通知”(Advice)执行。简单来说,它就像一个过滤器,从所有可能的连接点中筛选出你真正感兴趣的那些点。 例如,你可以定义一个切点,表示“所有以 Service 结尾的类中的所有公共方法”都是我想要增强的连接点。
    • 例子: 你想在所有用户管理相关的方法执行前打印日志,那么你的切点就可以定义为“所有 com.example.service.user 包下所有方法的执行”。
  3. 通知(Advice)

    • 通俗理解: 在选定的“连接点”上执行的“具体操作”或“增强逻辑”。它定义了“做什么”以及“何时做”。
    • 详细解释: 通知是切面在特定连接点执行的动作。它包含了实际的横切关注点逻辑,比如记录日志的代码、安全检查的代码、事务开始/提交/回滚的代码等。
    • 常见类型:
      • 前置通知(@Before): 在连接点执行之前运行,但不能阻止连接点继续执行。
      • 后置通知(@After): 无论连接点如何退出(正常返回或抛出异常),都会在连接点执行之后运行。
      • 返回通知(@AfterReturning): 在连接点正常成功执行并返回结果之后运行。
      • 异常通知(@AfterThrowing): 在连接点抛出异常之后运行。
      • 环绕通知(@Around): 包裹住连接点,可以在连接点执行前和执行后都进行操作,甚至可以控制连接点是否执行、修改返回值或参数。
  4. 切面(Aspect)

    • 通俗理解: 一个“模块”,它将“切点”和“通知”打包在一起。 可以理解为一个横切关注点的具体实现。
    • 详细解释: 切面是横切关注点的模块化单元。它通常是一个类,用特定的注解(如Spring中的 @Aspect)标识,它包含了通知(定义了要做什么)和切点(定义了在哪里做)。 一个切面可以包含多个通知和切点。
    • 例子: 你可以创建一个名为 LoggingAspect 的切面,它里面定义了一个切点(例如:所有业务层方法),以及一个前置通知(在方法执行前打印日志)和一个后置通知(在方法执行后打印日志)。

总结一下它们之间的关系:

你可以把你的程序想象成一条生产线,上面有很多工作站(连接点)。你想要在某些特定的工作站(通过切点规则筛选出来的连接点)进行额外的操作(通知)。那么,将这些“在哪儿操作”(切点)和“具体操作什么”(通知)打包在一起,就是一个“切面”。 AOP框架会在程序运行时,根据切面中定义的切点规则,在相应的连接点上执行通知中定义的增强逻辑,从而实现对核心业务逻辑的非侵入式增强。


1. 切点(PointCut)

代码中demo.controller路径下的所有类的方法都会统一执行下面的方法, "execution(* com.example.demo.controller..*.*(..))"(切点表达式)就是一个切点,用于描述那些方法需要进行增强:


@Aspect
@Component
public class TimeLogAspect {

    private static final Logger log = LoggerFactory.getLogger(TimeLogAspect.class);

    @Around("execution(* com.example.demo.controller..*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
      //.......
    }
}

2. 连接点(Join Point)

demo.controller 路径下的某一个具体的方法指的就是连接点(addBook、queryBookById等):

package com.example.demo.controller;

import org.springframework.web.bind.annotation.*;
import com.example.demo.entity.BookInfo;
import com.example.demo.common.Result;

@RestController
@RequestMapping("/book")
public class BookController {

    @RequestMapping("/addBook")
    public Result addBook(@RequestBody BookInfo bookInfo) {
        //...代码省略
        return Result.success();
    }

    @RequestMapping("/queryBookById")
    public Result queryBookById(@RequestParam Integer bookId) {
        //...代码省略
        return Result.success();
    }

    @RequestMapping("/updateBook")
    public Result updateBook(@RequestBody BookInfo bookInfo) {
        //...代码省略
        return Result.success();
    }
}

切点和连接点的关系:
连接点是满足切点表达式(@Around括号的内的字符串)的元素,切点是多个满足条件的连接点的集合。例如:
切点表达式:共青团
连接点:共青团中的某个共青团团员

3. 通知(Advice)

AOP需要从多个不同的业务代码中抽离出一些指定的有重复性的代码,这些重复代码就是通知的内容。
比如下面这个统计某个业务执行时间的代码就是通知:

@Around("execution(* com.example.demo.controller..*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
    // 记录方法执行开始时间
    long begin = System.currentTimeMillis();

    // 执行原始方法
    Object result = pjp.proceed();

    // 记录方法执行结束时间
    long end = System.currentTimeMillis();

    // 记录方法执行耗时
    log.info(pjp.getSignature() + " 执行耗时: {} ms", end - begin);

    return result;
}

4.切面(Aspect)

切面(Aspect) = 通知(Advice) + 切点(PointCut)

如下代码,TimeAspect类中的整个方法(recordTime)就是一个切面。切点通过@Round注解设置,通知就是时间的记录:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;


/**
 * 记录方法耗时的切面
 */
@Aspect
@Component
public class TimeAspect {

    /**
     * 记录方法耗时
     */
    @Around("execution(* com.example.demo.controller..*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // 记录方法执行开始时间
        long begin = System.currentTimeMillis();

        // 执行原始方法
        Object result = pjp.proceed();

        // 记录方法执行结束时间
        long end = System.currentTimeMillis();

        // 记录方法执行耗时
        log.info(pjp.getSignature() + " 执行耗时: {} ms", end - begin);

        return result;
    }
}

此外含有切面的类,我们称这个类角坐切面类,这个类会被@Aspect修饰

三、通知类型

第二节中的代码演示中@Round是其中通知注解,在Spring中通知类型有以下几种

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法在发生异常后执行

方法没有发生异常时,运行顺序:

在这里插入图片描述

方法发生异常时,运行顺序:
在这里插入图片描述

因为程序发生异常,所以AfterReturning和Around后不会执行.。一般@Round用的比较多,因为这个注解可以模拟实现其他注解的功能(执行时机)

注意事项:
@Round注解修饰的方法必须返回目标方法的返回值,如果目标方法没有返回值,返回null?

四、自定义注解实现AOP

  1. 定义一个注解:
@Retention(RetentionPolicy.RUNTIME)//表示运行时,仍然存在
@Target({ElementType.METHOD})//表示这个注解只能修饰方法
public @interface TimeRecord {
}

  1. 把这个注解申明到一个方法中(需要用到@annotation):
@Slf4j
@Aspect
@Component
public class TimeRecordAspect {

    @Around("@annotation(com.bite.aop.aspect.TimeRecord)")
//@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")//@annotation可以用到spring原生的注解中
    public Object timeRecord(ProceedingJoinPoint pjt){
        //1. 记录开始时间
        //2. 执行目标代码
        //3. 记录结束时间
        //4. 返回结果
        log.info("TimeRecordAspect 前");
        long start = System.currentTimeMillis();

        //执行目标方法
        Object o = null;
        try {
            o = pjt.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info("TimeRecordAspect 后");
        log.info(pjt.getSignature()+ "耗时: "+ (System.currentTimeMillis()-start) + "ms");
        return o;
    }
}
  1. 使用自定义的@TimeRecord注解
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {

    @TimeRecord//这是我们自己定义的注解
    @RequestMapping("/t1")
    public String t1(){//此方法将会被AOP代理,记录执行时间
        log.info("执行T1方法");
        return "t1";
    }



}

五、SpringAOP的实现原理

SpringAOP是通过动态代理对AOP编程思想的实现。
主要由两种方式实现:

  1. JDK动态代理
  2. CGlib动态代理。

代理模式: 代理模式是一种常见的设计模式。设计一个代理类,这个代理类可以控制目标对象的创建已经此对象目标方法的调用,这种简介调用的方式就是代理模式。
形象的解释: 比如我们想在APP上找房子租,APP客服就是一个租房代理,他联系各个房东看看有没有满足我们条件的房子,如果有,就把信息给我们,我们就可以去租。虽然他不是房产的拥有人,但是他帮我们成功租到了房子,这就是代理。
动态代理和静态代理的区别: 静态代理意思是在程序运行前,代理对象(.class文件)经已经创建了,动态代理是在程序运行时自动创建的(程序运行前并没有这个.class文件)

1. JDK动态代理

JDK动态代理运用的是Java的反射机制,在程序运行时创建代理对象,从而实现动态代理。但是它只能代理实现接口的类。 原因在于反射的一个核心方法的参数中需要传递目标类的接口数组:target.getClass().getInterfaces(),对于没有实现任何接口的目标类,这个参数无法传递。

2. CGlib动态代理

Spring会自动引入CGlib相关依赖。其动态代理是基于继承实现的,创建目标类的一个子类把增强逻辑植入子类。因此不能代理被final修饰的类或者方法,性能也要比原生的JDK低一些。但是其优点是可以代理没有实现任何接口的子类。

Spring Boot 2.x之后的版本统一使用了CGlib进行动态代理
Spring Frameword 有实现接口的类,用JDK动态代理实现,没有实现接口的类,用CGlib实现动态代理

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐