【Spring系列笔记】AOP

简介: 面向切面编程就是面向特定方法编程。通过将横切关注点(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 接口,用于在目标对象方法执行前后插入切面代码。


目录
相关文章
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
965 0
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
1824 13
|
XML Java 开发者
Spring Boot中的AOP实现
Spring AOP(面向切面编程)允许开发者在不修改原有业务逻辑的情况下增强功能,基于代理模式拦截和增强方法调用。Spring Boot通过集成Spring AOP和AspectJ简化了AOP的使用,只需添加依赖并定义切面类。关键概念包括切面、通知和切点。切面类使用`@Aspect`和`@Component`注解标注,通知定义切面行为,切点定义应用位置。Spring Boot自动检测并创建代理对象,支持JDK动态代理和CGLIB代理。通过源码分析可深入了解其实现细节,优化应用功能。
865 6
|
XML Java 数据格式
《深入理解Spring》:AOP面向切面编程深度解析
Spring AOP通过代理模式实现面向切面编程,将日志、事务等横切关注点与业务逻辑分离。支持注解、XML和编程式配置,提供五种通知类型及丰富切点表达式,助力构建高内聚、低耦合的可维护系统。
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
1000 1
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
1608 0
|
XML Java 测试技术
Spring AOP—通知类型 和 切入点表达式 万字详解(通俗易懂)
Spring 第五节 AOP——切入点表达式 万字详解!
1378 25
|
XML 安全 Java
Spring AOP—深入动态代理 万字详解(通俗易懂)
Spring 第四节 AOP——动态代理 万字详解!
714 24
|
XML Java 数据安全/隐私保护
Spring Aop该如何使用
本文介绍了AOP(面向切面编程)的基本概念和术语,并通过具体业务场景演示了如何在Spring框架中使用Spring AOP。文章详细解释了切面、连接点、通知、切点等关键术语,并提供了完整的示例代码,帮助读者轻松理解和应用Spring AOP。
690 2
Spring Aop该如何使用