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实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA打包应用程序的教程图解

    IDEA打包应用程序的教程图解

    这篇文章主要介绍了IDEA打包应用程序的教程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • Java中Thread.join()的使用方法

    Java中Thread.join()的使用方法

    这篇文章主要介绍了Java中Thread.join()的使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • 详解Java如何有效避免空指针

    详解Java如何有效避免空指针

    空指针,也就是NullPointerException 简称NPE的,怕一下子写出NPE,部分的伙伴看不懂,索性就改成了空指针,在实际的开发中,我们最讨厌的就是遇到空指针了,业务跑着跑着发现了空指针,所以本文详细介绍了Java如何有效的避免空指针,需要的朋友可以参考下
    2023-12-12
  • SpringCloud 2020-Ribbon负载均衡服务调用的实现

    SpringCloud 2020-Ribbon负载均衡服务调用的实现

    这篇文章主要介绍了SpringCloud 2020-Ribbon负载均衡服务调用的实现,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • Java线程基本使用之如何实现Runnable接口

    Java线程基本使用之如何实现Runnable接口

    这篇文章主要介绍了Java线程基本使用之如何实现Runnable接口问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • PowerJobAutoConfiguration自动配置源码流程解析

    PowerJobAutoConfiguration自动配置源码流程解析

    这篇文章主要为大家介绍了PowerJobAutoConfiguration自动配置源码流程解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • java 中的乱码问题汇总及解决方案

    java 中的乱码问题汇总及解决方案

    这篇文章主要介绍了java 中的乱码问题汇总相关资料,并附解决方案,出现乱码问题有编码与解码,字节流与字符流出现乱码,等其他情况,需要的朋友可以参考下
    2016-11-11
  • 详解 Java静态代理

    详解 Java静态代理

    这篇文章主要介绍了 Java静态代理的相关资料,帮助大家更好的理解和学习Java代理的知识,感兴趣的朋友可以了解下
    2020-08-08
  • Java基础-Java基本数据类型

    Java基础-Java基本数据类型

    这篇文章主要介绍了Java基础-Java基本数据类型,变量就是申请内存来存储值。也就是说,当创建变量的时候,需要在内存中申请空间,下面我们就来对Java基本数据类型作简单的介绍,需要的朋友可以参考一下
    2022-01-01
  • SpringBoot2.x入门教程之引入jdbc模块与JdbcTemplate简单使用方法

    SpringBoot2.x入门教程之引入jdbc模块与JdbcTemplate简单使用方法

    这篇文章主要介绍了SpringBoot2.x入门教程之引入jdbc模块与JdbcTemplate简单使用方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07

最新评论