Spring AOP 的组成和实现

 更新时间:2023年07月31日 10:16:53   作者:小鱼的学习笔记  
这篇文章主要介绍了Spring AOP 的组成和实现,AOP 是一种思想,Spring AOP 是这种思想的具体实现,本文结合实例代码给大家介绍的非常详细,需要的朋友可以参考下

1. Spring AOP 简介

AOP 是一种思想,Spring AOP 是这种思想的具体实现。

OOP:面向对象编程

AOP:面向切面编程

AOP 面向切面编程,就是对某一类事情的集中处理。

比如,我们需要在 CSDN 上进行编辑博客、发布博客、删除博客等操作,这些功能都是需要进行权限校验的,判断是否登录

开发三阶段

对于公共方法的处理:

  • (初级阶段)每个方法都去实现
  • (中级阶段)把同一类功能抽取成公共方法
  • (高级阶段)采用 AOP 的方式,对代码无侵入实现

除了统⼀的用户登录判断之外,AOP 还可以实现:

  • 统⼀日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等
统一方法执行时间统计项目监控:监控项目请求流量、监控接口的响应时间甚至每个方法的响应时间
统一的返回格式设置

httpstatus: HTTP状态码

code: 业务状态码(后端响应成功不代表业务办理成功) 

msg: 业务处理失败返回的信息

data: 

也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善

2. AOP 的组成

2.1 切面(Aspect)

切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合。

2.2 连接点(Join Point)

应⽤执行过程中能够插入切面的⼀个点,这个点可以是方法调用时、抛出异常时,甚至修改字段 时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

连接点相当于需要被增强的某个 AOP 功能的所有方法。

2.3 切点(Pointcut)

Pointcut 是匹配 Join Point 的谓词。 Pointcut 的作用就是提供⼀组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。

切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中⼀条⼀条 的数据)。

2.4 通知(Advice)

切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知

Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本 方法进行调用:

  • 前置通知使用 @Before:通知方法会在目标方法调用之前执行。
  • 后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
  • 返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
  • 抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
  • 环绕通知使用户 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。

切点相当于要增强的方法。 

AOP 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:

既然说 AOP 是对一类事情的集中处理,那么我们就需要明确两点:

  •  一类事情:处理对象的一个范围
  •  集中处理:处理的内容是什么

我们通过生活中的一个例子来看一下:

比如,我们乘坐高铁需要安检

那么,我们需要处理的内容就是安检;处理的范围就是需要乘坐高铁的人。

此处乘坐高铁需要安检这件事情就是切面,处理的内容安检就是通知,处理的范围乘坐高铁的人就是切点,具体有哪些人就是连接点

切点是一个规则,事情的处理,最终作用在方法上。 

3. Spring AOP的实现

3.1 新建项目

3.2 添加 AOP 框架支持 

在 pom.xml 中添加如下配置:

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

3.3 定义切面、切点和通知

 我们先定义 UserController 类:

@RequestMapping("/user")
@RestController
public class UserController {
    // 获取用户信息
    @RequestMapping("/getInfo")
    public String getInfo(){
        return "get info...";
    }
    // 注册
    @RequestMapping("/reg")
    public String reg(){
        return "reg...";
    }
    // Login
    @RequestMapping("/login")
    public String login(){
        return "login...";
    }
}

运行后,成功访问: 

接下来,我们在 UserController 类中定义切面和切点: 

@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    // 获取用户信息
    @RequestMapping("/getInfo")
    public String getInfo(){
        log.info("get info...");
        return "get info...";
    }
    // 注册
    @RequestMapping("/reg")
    public String reg(){
        log.info("reg...");
        return "reg...";
    }
    // Login
    @RequestMapping("/login")
    public String login(){
        log.info("login...");
        return "login...";
    }
}

在 LoginAspect 类中使用 @Before 注解(通知方法会在目标方法调用之前执行): 

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
}

我们接着新建一个 TestController 类:

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/hi")
    public String hi(){
        log.info("hi~");
        return "hi~";
    }
}

可以看到运行的结果中,并没有在控制台打印 @Before 中的内容: 

那么为什么没有执行呢?

我们再来看一下其他注解,@After(通知方法会在目标方法返回或者抛出异常后调用

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
    @After("pointcut()")
    public void doAfter(){
        log.info("do after...");
    }
}

运行结果如下:

@AfterReturning(通知方法会在目标方法返回后调用)

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
    @After("pointcut()")
    public void doAfter(){
        log.info("do after...");
    }
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        log.info("do after returning...");
    }
}

运行以上代码后: 

可以看到 :@AfterReturning 在 @After 之前被调用。

@AfterThrowing(通知方法会在目标方法抛出异常后调用)

我们首先在 UserController 类中加入异常:

@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    // 获取用户信息
    @RequestMapping("/getInfo")
    public String getInfo(){
        log.info("get info...");
        return "get info...";
    }
    // 注册
    @RequestMapping("/reg")
    public String reg(){
        log.info("reg...");
        int a = 10/0;
        return "reg...";
    }
    // Login
    @RequestMapping("/login")
    public String login(){
        log.info("login...");
        return "login...";
    }
}

添加 @AfterThrowing 注解:

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
    @After("pointcut()")
    public void doAfter(){
        log.info("do after...");
    }
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        log.info("do after returning...");
    }
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        log.info("do after throwing...");
    }
}

运行后可以看到: 

当正常返回时,执行 @AfterReturning 注解,当出现异常时,不会执行 @AfterReturning 注解;

当出现异常时,才会执行 @AfterThrowing 注解,当正常返回时,不会执行 @AfterThrowing 注解。

@Around(通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为):

添加  @Around 注解:

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
    @After("pointcut()")
    public void doAfter(){
        log.info("do after...");
    }
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        log.info("do after returning...");
    }
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        log.info("do after throwing...");
    }
    @Around("pointcut()")
    public void doAround(ProceedingJoinPoint joinPoint){
        log.info("环绕通知执行之前...");
        try {
            joinPoint.proceed(); // 调用目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info("环绕通知执行之后...");
    }
}

运行后界面显示如下:

可以看到此时界面中不再有返回值,因此修改代码如下:

@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
    Object oj = null;
    log.info("环绕通知执行之前...");
    try {
        oj = joinPoint.proceed(); // 调用目标方法
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
    log.info("环绕通知执行之后...");
    return oj;
}

此时可以看到成功返回并打印了值: 

我们再来看一下这段代码:

4. 切点表达式说明

AspectJ 支持三种通配符

  • * :匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)
  • .. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
  • + :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的 所有子类包括本身

切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为: 

execution(<修饰符><返回类型><包.类.方法(参数)><异常>)

5. 练习:使用 AOP 统计 UserController 每个方法的执行时间

@Slf4j
@Component
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
    public void pointcut(){}
    @Before("pointcut()")
    public void doBefore(){
        log.info("do berore...");
    }
    @After("pointcut()")
    public void doAfter(){
        log.info("do after...");
    }
    @AfterReturning("pointcut()")
    public void doAfterReturning(){
        log.info("do after returning...");
    }
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        log.info("do after throwing...");
    }
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object oj = null;
        log.info("环绕通知执行之前...");
        try {
            oj = joinPoint.proceed(); // 调用目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info("环绕通知执行之后...");
        return oj;
    }
    /**
     *
     * @param joinPoint 使用 AOP 统计 UserController 每个方法的执行时间
     * @return
     */
    @Around("pointcut()")
    public Object doAroundCount(ProceedingJoinPoint joinPoint){
        Object oj = null;
        long start = System.currentTimeMillis();
        try {
            oj = joinPoint.proceed(); // 调用目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info(joinPoint.getSignature().toString()+"耗时:"+(System.currentTimeMillis()-start));
        return oj;
    }
}

可以看到不同的方法直接在 url 中进行更改重新运行界面即可获得:

到此这篇关于Spring AOP 的组成和实现的文章就介绍到这了,更多相关Spring AOP实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中ArrayList删除的常用操作及方法

    Java中ArrayList删除的常用操作及方法

    ArrayList是最常用的一种java集合,在开发中我们常常需要从ArrayList中删除特定元素,本文主要介绍了Java中ArrayList删除的常用操作及方法,感兴趣的可以了解一下
    2023-11-11
  • IntelliJ IDEA 配置与插件全部迁移到其他盘彻底释放C盘空间

    IntelliJ IDEA 配置与插件全部迁移到其他盘彻底释放C盘空间

    这篇文章给大家介绍IntelliJ IDEA 配置与插件全部迁移到其他盘彻底释放C盘空间,本文将详细解释这一问题的根源,并提供两种实用的迁移方案,帮你把 IDEA 的所有用户数据安全地移到其他盘,彻底释放 C 盘空间,感兴趣的朋友跟随小编一起看看吧
    2026-03-03
  • SpringBoot解析JSON数据的三种方案

    SpringBoot解析JSON数据的三种方案

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成,本文给大家介绍了SpringBoot解析JSON数据的三种方案,需要的朋友可以参考下
    2024-03-03
  • Java对int[]数组做新增删除去重操作代码

    Java对int[]数组做新增删除去重操作代码

    这篇文章主要介绍了Java里面对int[]数组做新增删除去重实现,这里记录下使用int[]数组对数组进行新增删除去重等操作,用来更加了解java里面的集合类思想,需要的朋友可以参考下
    2023-10-10
  • Spring Boot 验证码框架 CAPTCHA详解

    Spring Boot 验证码框架 CAPTCHA详解

    这篇文章主要介绍了Spring Boot 验证码框架 CAPTCHA详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • Java 中的 CompletableFuture如何让异步编程变得简单

    Java 中的 CompletableFuture如何让异步编程变得简单

    CompletableFuture是Java 8引入的异步编程神器,它提供了强大的链式操作、任务组合和异常处理功能,使得异步编程变得简单优雅,本文给大家介绍Java中的CompletableFuture如何让异步编程变得简单,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • java枚举enum和Enum类的使用

    java枚举enum和Enum类的使用

    本文主要介绍了java枚举enum和Enum类的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Java web网站访问量的统计

    Java web网站访问量的统计

    这篇文章主要为大家详细介绍了Java web网站访问量的统计,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-01-01
  • Java使用TCP套接字实现多人聊天功能详解

    Java使用TCP套接字实现多人聊天功能详解

    这篇文章主要介绍了Java使用TCP套接字实现多人聊天功能,结合实例形式详细分析了java使用socket通信实现tcp协议下的聊天功能客户端与服务器端相关实现技巧,需要的朋友可以参考下
    2019-10-10
  • Java中的可重入锁ReentrantLock简析

    Java中的可重入锁ReentrantLock简析

    这篇文章主要介绍了Java中的可重入锁ReentrantLock简析,可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住,需要的朋友可以参考下
    2023-12-12

最新评论