【Spring系列笔记】AOP

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
函数计算FC,每月15万CU 3个月
应用实时监控服务-用户体验监控,每月100OCU免费额度
简介: 面向切面编程就是面向特定方法编程。通过将横切关注点(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
Spring Boot 3 集成Spring AOP实现系统日志记录
本文介绍了如何在Spring Boot 3中集成Spring AOP实现系统日志记录功能。通过定义`SysLog`注解和配置相应的AOP切面,可以在方法执行前后自动记录日志信息,包括操作的开始时间、结束时间、请求参数、返回结果、异常信息等,并将这些信息保存到数据库中。此外,还使用了`ThreadLocal`变量来存储每个线程独立的日志数据,确保线程安全。文中还展示了项目实战中的部分代码片段,以及基于Spring Boot 3 + Vue 3构建的快速开发框架的简介与内置功能列表。此框架结合了当前主流技术栈,提供了用户管理、权限控制、接口文档自动生成等多项实用特性。
22 8
|
2月前
|
XML Java 数据安全/隐私保护
Spring Aop该如何使用
本文介绍了AOP(面向切面编程)的基本概念和术语,并通过具体业务场景演示了如何在Spring框架中使用Spring AOP。文章详细解释了切面、连接点、通知、切点等关键术语,并提供了完整的示例代码,帮助读者轻松理解和应用Spring AOP。
Spring Aop该如何使用
|
2月前
|
监控 安全 Java
什么是AOP?如何与Spring Boot一起使用?
什么是AOP?如何与Spring Boot一起使用?
76 5
|
2月前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
80 8
|
2月前
|
Java 开发者 Spring
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
|
2月前
|
XML 监控 安全
深入调查研究Spring AOP
【11月更文挑战第15天】
50 5
|
2月前
|
Java 开发者 Spring
Spring AOP深度解析:探秘动态代理与增强逻辑
Spring框架中的AOP(Aspect-Oriented Programming,面向切面编程)功能为开发者提供了一种强大的工具,用以将横切关注点(如日志、事务管理等)与业务逻辑分离。本文将深入探讨Spring AOP的底层原理,包括动态代理机制和增强逻辑的实现。
51 4
|
3月前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
57 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
2月前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
46 1
|
4月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP