Spring AOP

目录

一、什么是AOP

[1.1 AOP的定义](#1.1 AOP的定义)

[1.2 为什么要有AOP](#1.2 为什么要有AOP)

[二、Spring AOP 应该学习哪些知识?](#二、Spring AOP 应该学习哪些知识?)

[三、Spring AOP的组成](#三、Spring AOP的组成)

[3.1 切面(Aspect,可以理解为"类")](#3.1 切面(Aspect,可以理解为"类"))

[3.2 切点(Pointcut,可以理解为"方法")](#3.2 切点(Pointcut,可以理解为"方法"))

[3.3 通知(Advice, 可以理解为方法的具体实现)](#3.3 通知(Advice, 可以理解为方法的具体实现))

[3.4 连接点(Join Point)](#3.4 连接点(Join Point))

[3.5 AOP概念图](#3.5 AOP概念图)

[四、Spring AOP 实现](#四、Spring AOP 实现)

[4.1 添加Spring AOP 框架支持](#4.1 添加Spring AOP 框架支持)

[4.2 定义切面,切点,通知](#4.2 定义切面,切点,通知)

[4.3 定义连接点](#4.3 定义连接点)

[4.4 关于环绕通知的扩展](#4.4 关于环绕通知的扩展)

[4.4.1 环绕通知与前后置通知的执行顺序](#4.4.1 环绕通知与前后置通知的执行顺序)

[4.4.2 环绕通知和CGLIB的关系](#4.4.2 环绕通知和CGLIB的关系)

[五、Spring AOP 实现原理](#五、Spring AOP 实现原理)

[5.1 AOP实现技术(静态代理,动态代理)](#5.1 AOP实现技术(静态代理,动态代理))

[5.2 SpringAOP为什么仅限于对方法级别的拦截](#5.2 SpringAOP为什么仅限于对方法级别的拦截)

[5.3 JDK动态代理和CGLIB动态代理的区别](#5.3 JDK动态代理和CGLIB动态代理的区别)

六、总结


一、什么是AOP

1.1 AOP的定义

在介绍Spring AOP之前,我们先来看看AOP是什么。

AOP(Aspect Oriented Programming): 面向切面编程,它是一种思想-> 是对某一类事情集中处理。

AOP是一种思想,SpringAOP则是一个框架,其提供了一种对AOP思想的实现,它们的关系和IoC与DI类似。

1.2 为什么要有AOP

比如用户登录权限的校验:如果没有AOP的存在,我们就需要在一个网站的某些场景下(只有用户登陆后才能访问的场景),再次对用户是否登录进行判断,这不仅增加了代码修改和维护的成本,而且不利于实现低耦合,无法让开发者把业务逻辑给区分开。

因此,对于以上场景,需要对这种使用次数多,功能又是相同的,就需要考虑AOP来进行统一处理。

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

  • 统一日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等

也就是说,AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP(Object Oriented

Programming,⾯向对象编程)的补充和完善。

二、Spring AOP 应该学习哪些知识?

  1. 学习AOP是如何组成的,也就是学习AOP组成的相关概念。
  2. 学习Spring AOP的使用
  3. 学习Spring AOP实现原理

三、Spring AOP的组成

3.1 切面(Aspect,可以理解为"类")

切面是由切点和通知组成的,它既包含了横切逻辑的定义,也包含了连接点的定义。

通俗来说,某一方面的具体内容就是一个切面,比如用户登录判断就是一个切面,而日志的统计记录也是一个切面。

3.2 切点(Pointcut,可以理解为"方法")

定义具体的拦截规则

3.3 通知(Advice, 可以理解为方法的具体实现)

定义了切面是什么,描述了切面要完成的工作,简单来说,就是 AOP的执行逻辑。

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

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

3.4 连接点(Join Point)

所有可能触发的点就叫做连接点。

3.5 AOP概念图

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

四、Spring AOP 实现

接下来我们使用Spring AOP来实现AOP的功能,大致的目标是 拦截所有UserController里面的方法,每次调用UserController中任意一个方法时,都执行相应的通知事件。

  1. 添加Spring AOP 框架支持
  2. 定义切面和切点
  3. 定义通知

4.1 添加Spring AOP 框架支持

去Maven仓库中找到Spring AOP的依赖(注意,寻找的其实是用于SpringBoot项目的SpringAOP依赖,如果是用于Spring项目的,Spirng自带的库中是已经添加过该依赖的)

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

java 复制代码
<!-- 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.10</version>
</dependency>

4.2 定义切面,切点,通知

java 复制代码
@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
}

4.3 定义连接点

java 复制代码
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;



@Controller
public class UserController {

    @RequestMapping("user/sayhi")
    public String sayHi() {
        System.out.println("执行了 sayHi 方法");
        return "hi, Spring boot aop";
    }
    @RequestMapping("/user/login")
    public String login() {
        System.out.println("执行了 login 方法");
        return "do user login";
    }
}

访问:localhost:8080/user/sayhi 后:控制台信息如下:

4.4 关于环绕通知的扩展

4.4.1 环绕通知与前后置通知的执行顺序

我们先来看一组代码:

java 复制代码
package com.example.demo.common;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知: " + LocalDateTime.now());
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始了");
        Object obj = joinPoint.proceed();
        System.out.println("环绕通知结束了");
        return obj;
    }
}
java 复制代码
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class UserController {

    @RequestMapping("user/sayhi")
    public String sayHi() {
        System.out.println("执行了 sayHi 方法");
        return "hi, Spring boot aop";
    }
    @RequestMapping("/user/login")
    public String login() {
        System.out.println("执行了 login 方法");
        return "do user login";
    }
}

执行结果如下:

我们发现,环绕通知始终是与最外侧的,这是因为joinPoint.proceed执行的是login方法本身,相应的,只有执行到方法本身时候,才会调用相对应的前置通知和后置通知。

因此也就有了这里环绕通知始终在最外侧的体现。

4.4.2 环绕通知和CGLIB的关系

使用以上程序代码,在 Object obj = joinPoint.proceed();处打一个断点,再次访问localhost:8080/user/login 就会发现,其内部是使用了CGLIB代理,通过这个代理调用了方法本身,也就是login方法。

如何利用环绕通知来统计方法的执行时间?

java 复制代码
package com.example.demo.common;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Aspect // 切面
@Component  // 不能忽略
public class UserAOP {
    // 切点 (配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") // 切点(配置拦截规则)
    public void pointcut() {
    }

    // 前置通知 @Before里面的参数取决于 切点的方法名
    @Before("pointcut()")
    public void doBefore() {
        System.out.println("执行了前置通知: " + LocalDateTime.now());
    }
    @After("pointcut()")
    public void doAfter() {
        System.out.println("执行了后置通知: " + LocalDateTime.now());
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始了");
        long startTime = System.currentTimeMillis();
        Object obj = joinPoint.proceed();
        String methodName = joinPoint.getSignature().getName();
        System.out.println("环绕通知结束了");
        long endTime = System.currentTimeMillis();
        long executionTime = endTime-startTime;
        System.out.println(methodName+"执行时间为: "+executionTime);
        return obj;
    }

}

运行结果:

五、Spring AOP 实现原理

Spring AOP 是构建在动态代理基础上,因此Spring 对 AOP 的支持仅限于方法级别的拦截。

Spring AOP 支持 JDK Proxy 和CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP会基于JDK生成代理类,没有实现接口的类,会基于CGLIB生成代理类。

织入(Weaving):代理的生成时机

织入是把切面应用到目标对象并创建新的代理对象的过程。

织入的过程可以在编译时,类加载时或运行时进行。根据织入的时间不同,有以下几种织入方式:

  • 编译时织入:在编译源代码成字节码的过程中,将切面代码织入到目标类中。例如 lombok就属于编译时织入。
  • 类加载时织入:在类加载过程中,通过类加载器将切面逻辑织入到目标类中。例如JVM在垃圾回收时有个类就属于类加载织入,其是在类加载过程中就织入到JVM中的。
  • 运行时织入:在目标对象的方法执行过程中,动态的将切面代码织入到方法的不同执行点。

SpringAOP就是属于运行时织入切面的。

5.1 AOP实现技术(静态代理,动态代理)

静态代理

静态代理是一种编译时就已经确认代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理的方法前后执行相应的操作。

静态代理的优点是实现简单,易于理解和掌握,但是缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时候,代理量会变得很大。

动态代理

动态代理是一种在运行时动态生成代理类的代理方式。在动态代理中,代理类不需要实现同一个接口或继承同一个父类,而是通过Java反射机制动态生成代理类,并在调用被代理类的方法前后执行相应的操作。

动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要理解JAVA反射机制和动态生成字节码的技术。

5.2 SpringAOP为什么仅限于对方法级别的拦截

  • JDK动态代理的限制: JDK动态代理只能代理实现了接口的类。在Spring AOP中,如果目标对象实现了接口,Spring将使用JDK动态代理来创建代理对象。由于JDK动态代理是基于接口实现的,它只能代理目标对象接口中声明的方法,无法代理目标对象内部的其他方法。
  • CGLIB动态代理的限制:CGLIB动态代理可以代理没有实现接口的类,在SpringAOP中,如果目标对象没有实现接口,Spring将使用CGLIB动态代理来创建代理对象,尽管CGLIB动态代理可以代理目标对象的所有方法,但是也存在限制,比如无法代理被标记为final的方法。

总结:

虽然 Spring AOP 的支持仅限于方法级别,但是这足以满足大部分应用中的拦截和增强需求。对于更复杂的场景,如果需要更精细的控制和更广泛的横切逻辑,可以考虑使用 AspectJ 等更强大的 AOP 框架。

5.3 JDK动态代理和CGLIB动态代理的区别

  • JDK动态代理和CGLIB动态代理都是最常见的动态代理实现技术,但它们有以下区别:
  • JDK动态代理基于接口,要求目标对象实现接口;CGLIB动态代理基于类,可以代理没有实现接口的目标对象。
  • JDK动态代理使用java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB动态代理使用CGLIB库生成代理对象
  • JDK动态代理生成的代理对象是目标对象的接口实现,CGLIB动态代理生成的代理对象是目标对象的子类
  • JDK动态代理性能相对较高,生成代理对象速度较快,CGLIB动态代理性能相对较低,生成代理对象速度较慢。
  • CGLIB动态代理无法代理final类和final方法,JDK动态代理可以代理任意类。

总结:

简单来说,JDK动态代理要求被代理类实现接口,而CGLIB要求被代理类不能是final修饰的最终类,在JDK8意思版本中,因为JDK动态代理做了专门的优化,所以它的性能比CGLIB高。

六、总结

AOP是对某方面能力的统一实现,它是一种实现思想,Spring AOP是对AOP的具体实现,Spring AOP可通过AspectJ(注解)的方式来实现AOP的功能,SpringAOP的实现步骤是:

  1. 添加AOP框架支持
  2. 定义切面和切点
  3. 定义通知

SpringAOP是通过动态代理的方式,在运行期将AOP织入到程序中,它的实现方式有两种:JDK Proxy 和CGLIB。

相关推荐
2401_8574396915 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧66616 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索18 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
weixin_4493108423 分钟前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
芒果披萨23 分钟前
Filter和Listener
java·filter
CodingBrother24 分钟前
MySQL 和 PostgreSQL 的使用案例
mysql·adb·postgresql
qq_49244844628 分钟前
Java实现App自动化(Appium Demo)
java
阿华的代码王国36 分钟前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话
standxy1 小时前
如何将钉钉新收款单数据高效集成到MySQL
数据库·mysql·钉钉
找了一圈尾巴1 小时前
前后端交互通用排序策略
java·交互