Spring AOP

简介: Spring AOP

引言



AOP ( Aspect Oriented Programming ) :面向切面编程


AOP 是一种思想,表示对某一类事情的集中处理。Spring AOP 是一个框架,它是对 AOP 思想的实现。


为什么要使用 AOP 思想?


想象一个场景,我们在做用户验证的时候,除了登录和注册不需要做用户验证之外,几乎其他所有页面都需要先验证用户登录的状态,验证成功后,后面的代码才能继续走下去。以往我们的处理方式,是将每个 Controller 都要写一遍用户验证,然而当功能越来越多的时候,即 Controller 越多的时候,那么我们要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会增加代码修改和维护的成本。此时,我们就可以考虑利用 AOP 来统一处理了。


而对于功能统一,且使用地方较多的功能,就可以优先考虑 AOP 思想。


一、AOP 组成



5557cc268f504ac2b27a12872c215273.png


1. 切面 (Aspect)

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


2. 连接点 (Join Point)

所有可能触发 AOP 的页面 / 数据,可以被称为连接点。


3. 切点 (Pointcut)

切点相当于保存了众多连接点的一个集合,如果把切点看作成一个表格,那么连接点就是表格中的一个个数据。另外,切点相当于增强的方法。


4. 通知 (Advice)

通知规定了 AOP 执行时机和执行方法。切面的工作就被称之为通知。


AOP 有五种通知:


① 前置通知

② 后置通知

③ 抛出异常之后的通知

④ 返回数据之后的通知

⑤ 环绕通知


二、Spring AOP 的使用



步骤1 添加依赖


由于在创建 Spring Boot 的时候,页面没有为我们提供 AOP 依赖,所以我们在 maven 仓库中,搜索如下依赖,并添加至 pom.xml 文件中。


d328772bd5f840beadcef17e4b80d2f6.png


<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
  <version>2.7.3</version>
</dependency>


步骤2 添加切面和切点


c6f079135eed421a8669a99fcb3afa78.png


切点指的是具体要处理的某一类问题,比如用户登录权限验证就是一个具体的问题;记录所有方法的执行日志就是一个具体的问题。


@Aspect // 当前类是一个切面
@Component
public class UserAspect {
    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {
    }
}


注意:


① 在代码中,我们通过 " @Pointcut " 这个注解来定义一个切点,并在注解的括号中注明表达式,表示拦截规则。


② point1 方法为空方法,它并不需要有方法体,它本身的方法名只是作为一个 " 标识 " 作用,这样后面的 Advice 通知就可以使用当前的切点。( 根据不同的拦截规则,切点可以设置多个 )


③ " @Pointcut " 注解后面的表达式遵循了 AspectJ 语法。


1b3e612375784bffbff1f952ad033df0.png

AspectJ 语法


execution() 是最常用的切点函数,用来匹配方法,比方说,我们需要对非法用户进行拦截,那么就可以在后端的方法代码中,利用 execution() 函数进行拦截,匹配到哪些方法,哪些方法就需要用来拦截非法用户。


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


其中,返回类型、方法、参数都不能省略。


AspectJ 支持的三种通配符:


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


示例:


execution(* com.cad.demo.User.*(..)) :匹配 User 类里的所有方法
execution(* com.cad.demo.User+.*(..)) :匹配该类的子类包括该类的所有方法
execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有方法
execution(* com.cad..*.*(..)) :匹配 com.cad 包下、子孙包下所有类的所有方法
execution(* addUser(String, int)) :匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是 int.


步骤3 添加 Advice 通知


在 UserAspect 类中,实现切面:


@Aspect // 当前类是一个切面
@Component
public class UserAspect {
    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {
    }
    // 1. 定义 point1 切点的前置通知
    @Before("point1()") // 选择 point1 切点,即遵循其拦截规则
    public void doBefore() {
        System.out.println("前置通知:被执行了");
    }
    // 2. 定义 point1 切点的后置通知
    @After("point1()")
    public void doAfter() {
        System.out.println("后置通知:被执行了");
    }
    // 3. 定义 point1 切点的返回之后通知
    @AfterReturning("point1()")
    public void doAfterReturning() {
        System.out.println("执行了 AfterReturning 方法");
    }
    // 4. 定义 point1 切点的异常通知
    @AfterThrowing("point1()")
    public void doAfterThrowing() {
        System.out.println("执行了 AfterThrowing 方法");
    }
    // 5.定义 point1 切点的环绕通知
    @Around("point1()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object result = null;
        System.out.println("环绕通知:开始");
        try {
            // 执行目标方法,以及目标方法所对应的相应的通知
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("环绕通知:结束");
        return result;
    }
}


在 UserController 类中,实现与前端交互:


@RestController
public class UserController {
    @RequestMapping("/hello1")
    public String hello1() {
        System.out.println("hello1 方法被执行了");
        return "你好,世界!";
    }
    @RequestMapping("/hello2")
    public String hello2() {
        System.out.println("hello2 方法被执行了");
        int a = 10 / 0;
        return "你好,世界!";
    }
}


前端访问 hello1 与 hello2 路径后,查看 IDEA 控制台:


6aaa97410cb74f7eaf31ae803b3acd9b.png


在上面的输出结果中,我们可以清晰地看到五个 Advice 通知的执行顺序。鉴于此,我们就可以在后端的代码中,对前端传来的参数进行一些处理,亦可以对后端即将要返回的数据进行优化。


比方说登录验证,我们就可以在前置通知中,设置一个拦截器,也就是说在前端与后端交互的 Controller 层之前,就可以直接拦截非法用户。


比方说异常处理,如果用户输入了一个参数,导致服务器异常,而此异常后端并没有事先意料到,我们就可以在后置通知中,将原本 500 的错误状态码封装成一个 json 数据给前端,前端再拿此 json 数据优化成一个用户看得懂的保存信息。(这是一件非常有意义的事情,因为用户并不是程序员,他们根本看不懂后端的错误提示,甚至连前端拿到 500 这样的错误信息,也不知道后端出现了什么问题。)


三、使用 AOP 统计某个类中每个方法的执行时间



对上面的代码做一些修改:


在 UserAspect 类中,实现切面:


@Aspect // 当前类是一个切面
@Component
public class UserAspect {
    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {
    }
    // 使用 AOP 统计 UserController 每个方法的执行时间
    @Around(("point1()"))
    public Object doAround2(ProceedingJoinPoint joinPoint) {
        // Spring 框架提供的 StopWatch,类似于 System.currentTimeMillis() 时间戳
        StopWatch stopWatch = new StopWatch();
        Object result = null;
        try {
            stopWatch.start();
            // 执行目标方法,以及目标方法所对应的相应的通知
            result = joinPoint.proceed();
            stopWatch.stop();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println(joinPoint.getSignature().getDeclaringType() + "." +
                            joinPoint.getSignature().getName() + " 方法花费的时间" +
                            stopWatch.getTotalTimeMillis()+ "ms");
        return result;
    }
}


在 UserController 类中,实现与前端交互:


@RestController
public class UserController {
    @RequestMapping("/hello2")
    public String hello2() {
        System.out.println("hello2 方法被执行了");
        return "你好,世界!";
    }
    @RequestMapping("/hello3")
    public String hello3() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("hello3 方法被执行了");
        return "hello, world!";
    }
}



2d10d29fc5694f4f809756415ffd52d7.png


四、Spring AOP 的实现原理



Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。我们知道,Spring Boot 单元测试的最小单元是方法级别的,所以可以看见,Spring AOP 的拦截功能做到了非常精准。


如下图所示,以往我们前端直接就可以与后端交互了,但是这就会带来安全隐患,当有 AOP 作为后端代理后,它就会在前端访问时,进行一些校验和拦截,显然数据在传输过程中更加安全。


994e809df7d742698af054ab05aa96a3.png


Spring AOP 动态代理实现的技术


1. JDK Proxy (JDK 动态代理)

2. GGLB Proxy


JDK Proxy 是官方提供的动态代理,但实际上它并不好用,所有 Spring AOP 一般来说会优先使用后者。当后者无法正常使用时,才会采用前者。


需要明确,GGLB Proxy 是通过继承代理对象来实现动态代理的,即子类拥有父类的所有功能的方式。但它不能代理最终类 (被final 修饰的类)。


目录
相关文章
|
3月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
20天前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
28 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
5天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
14 1
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
30天前
|
Java 编译器 Spring
Spring AOP 和 AspectJ 的区别
Spring AOP和AspectJ AOP都是面向切面编程(AOP)的实现,但它们在实现方式、灵活性、依赖性、性能和使用场景等方面存在显著区别。‌
49 2
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
117 9
|
29天前
|
XML Java 数据格式
Spring的IOC和AOP
Spring的IOC和AOP
43 0
|
2月前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
3月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
3月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
42 0
Spring高手之路22——AOP切面类的封装与解析