【Spring系列笔记】AOP

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 面向切面编程就是面向特定方法编程。通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,提供一种更好的代码模块化和可维护性。横切关注点指的是在应用程序中横跨多个模块或层的功能,例如日志记录、事务管理、安全性、缓存、异常处理等。

1. 概述

1.1. AOP定义

AOP(Aspect Oriented Programming),为面向切面编程或面向方面编程,是一种编程范式。旨在将交叉切入关注与作为业务主体的核心关注进行分离,以提高程序代码的模块化程度。

面向方面编程的核心概念,是从核心关注中分离出交叉切入关注。面向方面编程,在支配性分解的基础上,提供叫做方面(aspect)的一种辅助的模块化机制,这种新的模块化机制可以捕捉交叉切入关注。

(参考维基百科)

简单来说,面向切面编程就是面向特定方法编程。通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,提供一种更好的代码模块化和可维护性。

横切关注点指的是在应用程序中横跨多个模块或层的功能,例如日志记录、事务管理、安全性、缓存、异常处理等。

1.2. 特点

  1. 模块化和解耦合: 将横切关注点与核心逻辑解耦,提高代码的模块化程度。
  2. 降低代码复杂度: 简化核心逻辑,提高代码的可读性。
  3. 集中化关注点: 将多个模块间共同的关注点集中管理,提高代码的重用性。
  4. 代码无入侵:通过切入点表达式扫描对应方法进行动态代理增强,而不需要修改源代码

1.3. 应用

  1. 日志记录:记录方法的调用、参数和返回值,记录数据库的操作,用于调试和性能分析。
  2. 事务管理:管理事务的开始、提交、回滚等操作,确保数据的一致性和完整性。
  3. 权限校验:实施安全控制,例如身份验证和授权。
  4. 异常处理:捕获并处理异常,避免它们影响核心业务逻辑。
  5. 性能监控:监控方法的执行时间,以便识别性能瓶颈并进行优化。
  6. 数据缓存:实现数据的缓存,提高系统的响应速度。
  7. 追踪审计:记录用户操作,用于追踪和审计系统的使用情况。

1.4. SpringBootAOP

Spring AOP是Spring框架提供的一种AOP实现方式。AOP是一种编程范式,而Spring AOP是Spring框架对AOP的具体实现。

SpringBoot AOP是基于Spring AOP的另一种AOP实现方式,专门针对Spring Boot应用程序提供的一种简化配置和使用的方式。

1.5. AOP相关术语

切面(Aspect):是指横切多个对象的关注点的一个模块化,事务管理就是J2EE应用中横切关注点的很好示例。在Spring AOP中,切面通过常规类(基本模式方法)或者通过使用了注解@Aspect的常规类来实现。

连接点(Joint point):是指在程序执行期间的一个点,比如某个方法的执行或者是某个异常的处理。在Spring AOP中,一个连接点往往代表的是一个方法执行。

通知(Advice):是指切面在某个特殊连接点上执行的动作。通知有不同类型,包括 "around" ,"before" 和 "after" 通知。许多AOP框架包括Spring,将通知建模成一个拦截器,并且围绕连接点维持一个拦截器链。

切入点(Pointcut):是指匹配连接点的一个断言。通知是和一个切入点表达式关联的,并且在任何被切入点匹配的连接点上运行(举例,使用特定的名字执行某个方法)。AOP的核心就是切入点表达式匹配连接点的思想。Spring默认使用AspectJ切入点表达式语言

引入(Introduction):代表了对一个类型额外的方法或者属性的声明。Spring AOP允许引入新接口到任何被通知对象(以及一个对应实现)。比如,可以使用一个引入去使一个bean实现IsModified接口,从而简化缓存机制。(在AspectJ社区中,一个引入也称为一个inter-type declaration类型间声明)

目标对象(Target object):是指被一个或多个切面通知的那个对象。也指被通知对象("advised object"),由于Spring AOP是通过运行时代理事项的,这个目标对象往往是一个代理对象。

AOP 代理(AOP proxy):是指通过AOP框架创建的对象,用来实现切面合约的(执行通知方法等等)。在Spring框架中,一个AOP代理是一个JDK动态代理或者是一个CGLIB代理。

织入(Weaving):将切面和其他应用类型或者对象连接起来,创建一个被通知对象。这些可以在编译时(如使用AspectJ编译器)、加载时或者运行时完成。Spring AOP,比如其他纯Java AOP框架一般是在运行时完成织入。

名称

说明

连接点

可以被AOP控制的方法

通知

指那些重复的逻辑,也就是共性功能(最终体现为一个方法)

切入点

匹配连接点的条件,通知仅会在切入点方法执行时被应用,在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点

切面

当通知和切入点结合在一起,就形成了一个切面。

目标对象

通知所应用的对象(被AOP加强的对象)

切面类

被@Aspect注解标识的类,切面所在位置

AOP 代理

通过AOP框架创建的对象,被AOP加强的方法本质是AOP框架常见一个代理类继承原始类,重写被AOP加强/控制的方法

1.6. AOP Advice 相关术语

  • 前置通知(Before advice):在一个连接点之前执行的通知。但这种通知不能阻止连接点的执行流程(除非它抛出一个异常)
  • 后置返回通知(After returning advice):在一个连接点正常完成后执行的通知(如,如果一个方法没有抛出异常的返回)
  • 后置异常通知(After throwing advice):在一个方法抛出一个异常退出时执行的通知。
  • 后置通知(After(finally) advice):在一个连接点退出时(不管是正常还是异常返回)执行的通知。
  • 环绕通知(Around advice):环绕一个连接点的通知,比如方法的调用。这是一个最强大的通知类型。环绕通知可以在方法调用之前和之后完成自定义的行为。也负责通过返回自己的返回值或者抛出异常这些方式,选择是否继续执行连接点或者简化被通知方法的执行。

2. SpringBoot AOP入门

2.1. 基本步骤

  1. 导入依赖
<!--Aop依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写切面类
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component   // 把当前类的对象交给spring容器去管理
@Aspect  // 代表当前是一个切面类
@Slf4j  //日志
public class TimeAspect {
    //配置规则
    @Around("execution(* com.system.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint jp) throws Throwable{
        long start = System.currentTimeMillis();
        Object obj = jp.proceed();
        long end = System.currentTimeMillis();
        log.info("执行方法用了:{}ms",end - start);
        return obj;
    }
}

2.2. 流程解析:

底层通过动态代理形式,创建一个子类去继承接口实现类,重写接口类的方法。

2.3. 通知类型

在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。

其他通知类型对应的注解如下:

@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

@Before:前置通知,此注解标注的通知方法在目标方法前被执行

@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行

@PointCut:将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

@Pointcut("execution(* com.itheima.service.*.*(..))") // 抽取切入点表达式
public void pt(){}
@Before("pt()")
public void before(JoinPoint jp){
    System.out.println("执行了before");
}
@After("pt()")
public void after(JoinPoint jp){
    System.out.println("执行了after");
}
@AfterReturning("pt()")
public void afterReturning(JoinPoint jp){
    System.out.println("执行了AfterReturning");
}
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint jp){
    System.out.println("执行了AfterThrowing");
}

注意事项:

  1. @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。

2.4. 通知顺序

当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。这多个通知方法到底哪个先运行,哪个后运行?

结论:

  1. 不同切面类中,默认按照切面类的类名字母排序;
  2. 可使用@Order注解,控制通知的执行顺序;@Order()内数字从小到大执行,从大到小退出;类似于过滤器链。
@Slf4j
@Component
@Aspect
@Order(2)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
    //前置通知
    @Before("execution(* com.system.service.*.*(..))")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }
    //后置通知 
    @After("execution(* com.system.service.*.*(..))")
    public void after(){
        log.info("MyAspect2 -> after ...");
    }
}

2.5. 切入点表达式

2.5.1. execution

execution(......):用于匹配特定方法名的方法

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)
● 其中带 ? 的表示可以省略的部分
● 访问修饰符:可省略(比如: public、protected)
● 返回值:常使用*代替,表示任意类型
● 包名.类名: 可省略(不建议省略)
● 方法名:需正确
● 方法参数: 常使用..表示,表任意类型
● throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,
也可以通配包、类、方法名的一部分
.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
//全称
//假如指向DeptController内方法add(Dept dept)
@Around("execution(public com.system.pojo.Result  com.system.controller.DeptController.add(com.system.pojo.Dept) )")
//假如指向DeptService内方法findById(Integer id)
@Before("execution(public com.system.pojo.Dept com.system.service.DeptService.findById(java.lang.Integer))")
//常见
 @Around("execution(* com.system.controller.*.*(..))")

2.5.2. annotation

@annotation(……) :用于匹配标识有特定注解的方法。

1.自定义注解
================================================
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
public @interface MyAnno {
}
2.标记方法
================================================
 @MyAnno
    @Override
    public Dept findById(Integer id) {
        return deptMapper.findById(id);
    }
3.切面表达式连接
================================================
 @Before("@annotation(com.system.anno.MyAnno)")
    public void before(JoinPoint jp){
        System.out.println("执行了before");
    }

注意事项:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。
  2. 切入表达式通常关注接口,而非其接口实现类,增强拓展性。
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。可将execution和annatation搭配使用。
  • 包名匹配尽量不使用 ..
  • 使用 * 匹配单个包
  • 不建议省略类名.包名

2.6. 连接点

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。其中:

对于 @Around 通知,获取连接点信息只能使用  ProceedingJoinPoint

对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型

import com.system.anno.MyAnno;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.util.Arrays;
@Component
@Aspect
@Slf4j
@Order(1)
public class DemoAspect {
    @Pointcut("execution(* com.system.service.*.*(..))") // 抽取切入点表达式
    public void pt(){}
    //通过 ProceedingJoinPoint 获取方法运行相关信息
    @Around("pt()")
    public Object arround(ProceedingJoinPoint jp) throws Throwable {
        Object obj = jp.getTarget(); // 获取目标对象【被代理的对象】
        String className = obj.getClass().getSimpleName(); // 获取类名
        String name = obj.getClass().getName(); // 获取全类名
        Signature signature = jp.getSignature(); // 获取方法的签名
        String methodName = signature.getName(); // 获取方法名
        Object[] args = jp.getArgs(); // 获取方法执行的参数
        Object res = jp.proceed(); // 执行完方法,得到方法的返回值
        System.out.println("obj = " + obj);
        System.out.println("className = " + className);
        System.out.println("name = " + name);
        System.out.println("signature = " + signature);
        System.out.println("methodName = " + methodName);
        System.out.println("args:" + Arrays.toString(args));
        System.out.println("res:" + res);
        return res;
    }
/*
        obj = com.itheima.service.impl.DeptServiceImpl@581fb6b9
        className = DeptServiceImpl
        signature = List com.itheima.service.impl.DeptServiceImpl.findAll()
        methodName = findAll
        args:[]
        res:[Dept(id=1, name=学66, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T16:35:43),
        Dept(id=2, name=教研部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52),
        Dept(id=3, name=咨询部4, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T22:49:06),
        Dept(id=4, name=就业部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52),
        Dept(id=5, name=人事部, createTime=2024-02-03T15:36:52, updateTime=2024-02-03T15:36:52)]
 */
}

3. 底层原理

Spring AOP 的实现原理是基于动态代理和字节码操作的。在编译时, Spring 会使用 AspectJ 编译器将切面代码编译成字节码文件。在运行时, Spring 会使用 Java 动态代理或 CGLIB 代理生成代理类,这些代理类会在目标对象方法执行前后插入切面代码,从而实现AOP的功能。

Spring AOP 可以使用两种代理方式:JDK动态代理和 CGLIB 代理。如果目标对象实现了至少一个接口,则使用JDK动态代理;否则,使用 CGLIB 代理。SpringBoot默认使用 CGLIB 代理。

3.1. JDK动态代理

public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
    private final AdvisedSupport advised;
    public JdkDynamicAopProxy(AdvisedSupport advised) {
        this.advised = advised;
    }
    @Override
    public Object getProxy() {
        return Proxy.newProxyInstance(
                getClass().getClassLoader(),
                advised.getTargetSource().getInterfaces(),
                this
        );
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MethodInterceptor methodInterceptor = advised.getMethodInterceptor();
        MethodInvocation methodInvocation = new ReflectiveMethodInvocation(
                advised.getTargetSource().getTarget(),
                method,
                args,
                methodInterceptor,
                advised.getTargetSource().getTargetClass()
        );
        return methodInvocation.proceed();
    }
}

在该代码中,JdkDynamicAopProxy 类实现了 AopProxy 和 InvocationHandler 接口。getProxy 方法返回一个代理对象,该代理对象实现了目标对象实现的所有接口。invoke 方法用于执行代理方法,该方法会在目标对象方法执行前后插入切面代码。

3.2. CGLIB动态代理

CGLIB 代理是一个基于字节码操作的代理方式,它可以为没有实现接口的类创建代理对象。CGLIB 代理会在运行时生成一个目标对象的子类,并覆盖其中的方法,以实现AOP的功能。

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor {
    private Object target;
    public Object getProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass()); // 设置代理目标
        enhancer.setCallback(this); // 设置回调
        return enhancer.create(); // 创建代理对象
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("方法调用前的处理");
        Object result = proxy.invokeSuper(obj, args); // 调用目标对象的方法
        System.out.println("方法调用后的处理");
        return result;
    }
}

在该代码中,CglibAopProxy 类实现了 AopProxy 接口。getProxy 方法返回一个代理对象,该代理对象是目标对象的子类,并覆盖了其中的方法。DynamicAdvisedInterceptor 类实现了 MethodInterceptor 接口,用于在目标对象方法执行前后插入切面代码。


目录
相关文章
|
1天前
|
运维 Java 程序员
Spring5深入浅出篇:基于注解实现的AOP
# Spring5 AOP 深入理解:注解实现 本文介绍了基于注解的AOP编程步骤,包括原始对象、额外功能、切点和组装切面。步骤1-3旨在构建切面,与传统AOP相似。示例代码展示了如何使用`@Around`定义切面和执行逻辑。配置中,通过`@Aspect`和`@Around`注解定义切点,并在Spring配置中启用AOP自动代理。 进一步讨论了切点复用,避免重复代码以提高代码维护性。通过`@Pointcut`定义通用切点表达式,然后在多个通知中引用。此外,解释了AOP底层实现的两种动态代理方式:JDK动态代理和Cglib字节码增强,默认使用JDK,可通过配置切换到Cglib
|
1天前
|
XML 监控 安全
Spring特性之一——AOP面向切面编程
Spring特性之一——AOP面向切面编程
14 1
|
1天前
|
Java Spring 容器
Spring AOP浅谈
Spring AOP浅谈
10 1
|
1天前
|
XML Java 数据格式
Spring高手之路18——从XML配置角度理解Spring AOP
本文是全面解析面向切面编程的实践指南。通过深入讲解切面、连接点、通知等关键概念,以及通过XML配置实现Spring AOP的步骤。
22 6
Spring高手之路18——从XML配置角度理解Spring AOP
|
1天前
|
XML Java 数据格式
Spring使用AOP 的其他方式
Spring使用AOP 的其他方式
16 2
|
1天前
|
XML Java 数据格式
Spring 项目如何使用AOP
Spring 项目如何使用AOP
23 2
|
1天前
|
Java 开发者 Spring
Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
【5月更文挑战第1天】Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
25 5
|
1天前
|
XML Java 数据格式
Spring AOP
【5月更文挑战第1天】Spring AOP
28 5
|
1天前
|
Java 编译器 开发者
Spring的AOP理解
Spring的AOP理解
|
1天前
|
XML Java 数据格式
如何在Spring AOP中定义和应用通知?
【4月更文挑战第30天】如何在Spring AOP中定义和应用通知?
17 0

热门文章

最新文章

http://www.vxiaotou.com