欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 幼教 > Spring AOP相关知识详解

Spring AOP相关知识详解

2024/11/30 2:53:16 来源:https://blog.csdn.net/weixin_42970433/article/details/144089029  浏览:    关键词:Spring AOP相关知识详解

文章目录

  • 1.AOP介绍
    • 1.1 面向切面编程 - Aspect Oriented Programming (AOP)
    • 1.2 优点
  • 2.AOP的概念
    • 2.1 连接点、切入点、通知、切面:
    • 2.2 注解
      • 2.2.1 通知类型
        • 2.2.1.1 通知的优先级排序
      • 2.2.2 其他重要注解
      • 2.2.3 示例代码(四种通知)
  • 3.Spring AOP配置方式
  • 4.AOP的应用
    • 4.1 日志
    • 4.2 异常处理
    • 4.3 在spring中aop的应用是事务
      • 4.3.1 事务的传播行为
      • 4.3.2 事务的隔离级别
      • 4.3.3 事务的配置方式
    • 4.4 在具体项目中的应用
  • 5.aop底层实现
    • 5.1 动态代理
      • 5.1.1 JDK动态代理(实现和被代理类一样的接口)
      • 5.1.2 CGLIB动态代理(创建目标类的子类)
      • 5.1.3 两种代理的区别
    • 5.2 后置处理器相关
      • 5.2.1 后置处理器
      • 5.2.2 增强器(封装切面的信息)
      • 5.2.3 在 getBean 时的创建流程
  • 6.设计模式
    • 6.1 代理模式
    • 6.2 责任链模式(依次调用增强器的通知方法)
  • 7.aop失效
    • 7.1 aop失效的场景
      • 7.1.1 同一个类内部中,不同方法间的调用
      • 7.1.2 多线程环境下,线程直接调用目标对象方法
      • 7.1.3 final 修饰的方法不能被重写
      • 7.1.4 private方法由于访问权限
    • 7.2 解决方法

1.AOP介绍

1.1 面向切面编程 - Aspect Oriented Programming (AOP)

AOP,作为OOP面向对象编程的一种功能补充。AOP是Spring框架重要的组件,作为IOC的补充。

是一种思想,本质是为了解耦。将横切逻辑代码抽取出去,通过简单的配置,使用动态代理的方式添加到目标方法中,做到无侵入、便捷。

Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦。面向切面编程是一种编程范式,主要用于分离那些横切多个业务模块的关注点,比如日志记录、事务管理、权限验证等这些功能就属于横切关注点。

关注点是指在软件开发过程中,系统中某个特定的业务功能、特性、需求或者设计,是开发人员重点关注、考虑、实现的部分。

横切关注点是跨越多个模块、多个层次或者多个业务功能的关注点

1.2 优点

横向解决代码重复的问题
(1)解耦横切关注点:将与业务逻辑不直接相关的功能,横切的功能解耦。如日志记录、事务管理、权限验证等从业务代码中分离出来,使得业务逻辑更加清晰,易于理解和维护。

(2)代码复用:多个业务模块可以共用同一个切面,避免了代码的重复编写。

(3)灵活、可扩展:可以方便地添加、删除或修改切面的逻辑,而不需要对业务逻辑进行大规模的改动。

2.AOP的概念

我之前的一篇博文写的

2.1 连接点、切入点、通知、切面:

(1)连接点:可以成为切入点

(2)切入点(在哪干):需要增强的目标方法。用来确定在哪些地方应用切面的逻辑,比如可以定义在某个类的所有方法上,或者某个包下的特定方法上。

(3)通知(干什么):是定义在切面中的一段代码,指定了在切面的切点位置上要执行的具体动作逻辑。(什么时候做、做什么)。通知是在目标方法执行前、后执行的方法。通知用于在目标方法执行的特定时机插入额外的行为,从而实现对目标对象方法的增强或扩展。通知是在目标方法执行前、后执行的方法。比如前置通知可以在目标方法执行前做一些事情,后置通知在目标方法执行后做一些事情,环绕通知则可以在目标方法执行前后都进行相应的操作。

(4)切面=切入点+通知。切面是对横切关注点的抽象,它把切点和通知组合在一起。

(5)织入:织入把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。就是通过动态代理对目标对象方法进行增强的过程。运行时织入。

(6)目标对象:被一个或者多个切面所通知的对象,也被称作被通知对象。Spring AOP是通过运行时代理实现的,所以这个对象永远是一个被代理的对象。

2.2 注解

2.2.1 通知类型

(1)@Before 前置通知(Before Advice)
定义:在目标方法执行之前执行的通知。
作用:常用于进行一些准备工作,如参数校验、权限判断、初始化操作等。例如,在一个用户登录的方法前,使用前置通知可以进行用户名和密码的非空校验,若校验不通过则直接返回错误信息,不执行后续的登录逻辑。
(2)@After 后置通知(After Advice)
定义:在目标方法执行之后执行的通知,无论目标方法是否抛出异常都会执行。
作用:可用于进行一些资源清理、日志记录等操作。比如在一个文件上传的方法执行后,使用后置通知可以关闭文件流,释放相关资源,或者记录文件上传的结果信息,无论上传是否成功都可以进行相应的记录。
(3)@AfterReturning 正常返回通知(After Returning Advice)
定义:在目标方法正常执行完成并返回结果后执行的通知。
作用:可以对目标方法的返回值进行处理或记录。例如,在一个查询数据库获取用户信息的方法后,使用返回通知可以对查询到的用户信息进行加密处理,或者记录查询结果以便进行数据分析。
(4)@After-Throwing 异常通知(After Throwing Advice)
定义:在目标方法执行过程中抛出异常时执行的通知。
作用:主要用于处理异常情况,如记录异常信息、进行异常的统一处理或回滚事务等。比如在一个数据更新的方法中,如果出现数据库连接异常或数据冲突异常,异常通知可以捕获并记录这些异常,同时可以根据异常类型进行相应的处理,如提示用户重新操作或回滚到之前的状态。
(5)@Around 环绕通知(Around Advice)
定义:环绕通知可以在目标方法执行前后都添加自定义逻辑,它将目标方法的执行包裹在其中,是最强大也最灵活的一种通知类型。环绕通知是最常用的通知类型。
作用:可以决定目标方法是否执行、何时执行以及如何执行,还可以对目标方法的参数和返回值进行修改。例如,在一个远程服务调用的方法上添加环绕通知,可以在调用前进行网络连接的检查和参数的预处理,在调用后对返回结果进行缓存处理或错误处理等 。

@Aspect
public class PerformanceAspect {@Around("execution(* com.example.service..*.*(..))")public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable {long startTime = System.currentTimeMillis();Object result = pjp.proceed();long endTime = System.currentTimeMillis();System.out.println("方法执行时间: " + (endTime - startTime) + " 毫秒");return result;}
}
2.2.1.1 通知的优先级排序

优先级:异常通知>后置通知>前置通知>后置返回通知。
(只要异常通知报错,无论前置通知、后置通知、后置返回通知报错,都将返回的是异常通知)

如果有多个通知想要在同一连接点运行会发生什么?
在“进入”连接点的情况下,最高优先级的通知会先执行(所以给定的两个前置通知中,优先级高的那个会先执行)。
在“退出”连接点的情况下,最高优先级的通知会最后执行。(所以给定的两个后置通知中, 优先级高的那个会第二个执行)。

2.2.2 其他重要注解

@Aspect:用来定义一个类作为切面。
@Pointcut:用于定义切点的位置。用于定义切点表达式,将切点表达式单独提取出来,以便在多个通知中重复使用,提高代码的可维护性。

@Aspect
public class LoggingAspect {@Pointcut("execution(* com.example.service..*.*(..))")public void serviceMethods() {}@Before("serviceMethods()")public void beforeServiceMethod() {System.out.println("服务方法执行前的日志记录");}
}

2.2.3 示例代码(四种通知)

@Aspect
public class Logging {/** Following is the definition for a pointcut to select*  all the methods available. So advice will be called*  for all the methods.*/@Pointcut("execution(* com.tutorialspoint.*.*(..))")private void selectAll(){}@Before("selectAll()")public void beforeAdvice(){System.out.println("Going to setup student profile.");}@After("selectAll()")public void afterAdvice(){System.out.println("Student profile has been setup.");}@AfterReturning(pointcut = "selectAll()", returning="retVal")public void afterReturningAdvice(Object retVal){System.out.println("Returning:" + retVal.toString() );}@AfterThrowing(pointcut = "selectAll()", throwing = "ex")public void AfterThrowingAdvice(IllegalArgumentException ex){System.out.println("There has been an exception: " + ex.toString());   }  
}

3.Spring AOP配置方式

支持XML模式、基于@AspectJ注解的两种配置方式。

未详细介绍

4.AOP的应用

4.1 日志

以我们常见的电商系统为例,比如说用户登录、商品下单、订单查询等这些业务模块,都可能需要记录操作日志,而日志记录这个功能并不属于具体某个业务模块的核心逻辑,但却贯穿于多个业务模块之中,这时候就适合使用 AOP 来处理。

4.2 异常处理

我的一篇博客

4.3 在spring中aop的应用是事务

别人写的博客
Spring AOP 为事务管理提供了一种非侵入式的实现方式,将事务管理这一横切关注点从业务逻辑代码中分离出来。
Spring 通过 AOP 的动态代理机制,为被事务管理的方法创建代理对象。当调用这些被代理的方法时,Spring 会根据方法上的事务配置,自动启动、提交或回滚事务。例如,我们常用的@Transactional注解就是基于 Spring AOP 实现事务管理的典型体现 。当一个方法被标记上@Transactional注解时,Spring AOP 会拦截该方法的调用,并在方法执行前开启一个事务,在方法正常执行完成后提交事务,如果方法执行过程中抛出异常,则会自动回滚事务,确保数据的一致性。

本质是依靠Spring框架提供的Bean生命周期相关回调接口和AOP结合完成的,简述如下:
1.通过自动代理创建器依次尝试为每个放入容器中的bean尝试进行代理
2.尝试进行代理的过程对于事务管理来说,就是利用事务管理涉及到的增强器advisor,即TransactionAttributeSourceAdvisor
3.判断当前增强器是否能够应用在当前bean上,怎么判断呢? —> advisor内部的pointCut!
4.如果能够应用,那么好,为当前bean创建代理对象返回,并且往代理对象内部添加一个TransactionInterceptor拦截器。
5.此时我们再从容器中获取,拿到的就是代理对象了,当我们调用代理对象的方法时,首先要经过代理对象内部拦截器链的处理,处理完后,最终才会调用被代理对象的方法。(这里其实就是责任链模式的应用)

4.3.1 事务的传播行为

在 Spring 事务管理中,事务的传播行为定义了多个事务方法之间相互调用时,事务如何在这些方法间传播。
例如,REQUIRED传播行为是最常用的一种,如果当前没有事务,就新建一个事务;如果已经存在一个事务,则加入到这个事务中。这确保了在一个业务流程中,多个相关的数据库操作可以在同一个事务的控制下,保证数据的一致性。
REQUIRES_NEW传播行为,它总是会开启一个新的事务,而不管当前是否已经存在事务,新事务与原有事务相互独立。

4.3.2 事务的隔离级别

READ_UNCOMMITTED(允许读取未提交的数据,可能导致脏读)、READ_COMMITTED(只能读取已提交的数据,避免脏读,但可能出现不可重复读)
REPEATABLE_READ(在同一个事务中多次读取的数据是一致的,可防止不可重复读,但可能出现幻读)
SERIALIZABLE(最高的隔离级别,完全串行化执行事务,可避免所有并发问题,但性能较差)

4.3.3 事务的配置方式

基于注解。通过在方法或类上添加@Transactional注解,可以轻松地将该方法或类纳入事务管理的范畴。注解中可以配置事务的各种属性,如传播行为、隔离级别、超时时间等。
此外,Spring AOP 还允许我们在配置文件或者基于 Java 的配置类中对事务进行全局配置,设置默认的事务属性,如默认的传播行为、隔离级别等。

4.4 在具体项目中的应用

在项目中的应用、使用场景

5.aop底层实现

Java中的静态代理和动态代理

5.1 动态代理

什么是动态代理:动态代理就是在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。
在这里插入图片描述

Spring的代理方式有两种,一种是 JDK动态代理,一种是CGLIB代理。Spring默认是JDK动态代理。
以下两种情况使用CGLIB代理:
①@EnableAspectJAutoProxy(proxyTargetClass = true) 强制要求Spring使用CGLIB代理。
②被代理的类不是接口的实现类。

5.1.1 JDK动态代理(实现和被代理类一样的接口)

JDK 动态代理是利用 Java 反射机制,在运行时动态创建代理类和代理对象的。它要求被代理的类必须实现一个或多个接口。代理类会实现与被代理类相同的接口,并在调用方法时通过反射机制转发到被代理类的相应方法上,同时可以在方法调用前后添加额外的逻辑。
使用场景:当被代理的类已经实现了接口,只需要对接口中定义的方法进行代理增强时。
总结:通过反射创建代理对象,在方法调用时通过invoke方法(反射)调用被代理类的方法,并且可以添加额外逻辑。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 定义接口
interface IService {//用来调用的方法void doSomething();
}
// 实现接口的真实类
class ServiceImpl implements IService {//被代理对象的方法@Overridepublic void doSomething() {System.out.println("执行具体业务逻辑");}
}
// 动态代理处理器
class MyInvocationHandler implements InvocationHandler {private Object target;public MyInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("在方法调用前执行额外逻辑,如日志记录或权限验证");// 调用目标对象的方法Object result = method.invoke(target, args);System.out.println("在方法调用后执行额外逻辑,如结果处理或资源清理");return result;}
}
//流程
public class JDKDynamicProxyExample {public static void main(String[] args) {// 创建真实对象IService realService = new ServiceImpl();// 创建动态代理对象IService proxyService = (IService) Proxy.newProxyInstance(realService.getClass().getClassLoader(),realService.getClass().getInterfaces(),new MyInvocationHandler(realService));// 调用代理对象的方法proxyService.doSomething();}
}

5.1.2 CGLIB动态代理(创建目标类的子类)

我写的很不具体

Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截。
它不需要类实现接口,可以直接为普通的类创建代理。
它通过在运行时动态生成字节码来创建代理类,与基于接口的 JDK 动态代理不同。CGLIB 会在运行时生成一个被代理类的子类,然后在这个子类中重写被代理类的方法,通过在重写的方法中添加一些额外的逻辑来实现代理的功能。

代码中的流程:1.创建一个Enhancer对象,用来创建代理对象。2.在这个对象上,设置要被代理的类。3.设置拦截器,这个拦截器是我们写的,拦截器里面是具体的功能:在方法执行前后的操作,调用方法。4.用Enhancer创建代理对象,通过代理对象调用目标方法。

举个例子🌰

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 这是我们要代理的目标类
class UserService {public void addUser(String username) {System.out.println("添加用户: " + username);}
}
// 这是实现了MethodInterceptor接口的拦截器类,用来添加代理逻辑
class MyMethodInterceptor implements MethodInterceptor {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {// 在调用目标方法之前添加的逻辑,这里是打印日志System.out.println("开始执行方法: " + method.getName());// 调用目标方法,通过methodProxy的invokeSuper方法来调用,注意这里的o是代理对象Object result = methodProxy.invokeSuper(o, objects);// 在调用目标方法之后添加的逻辑,这里也是打印日志System.out.println("方法执行完毕: " + method.getName());return result;}
}
//调用的例子 
public class CglibProxyExample {public static void main(String[] args) {// 1.创建Enhancer对象,它是CGLIB的核心类,用来创建代理对象Enhancer enhancer = new Enhancer();// 2.设置要代理的类,也就是UserService类enhancer.setSuperclass(UserService.class);// 3.设置拦截器,也就是我们刚才写的MyMethodInterceptor类enhancer.setCallback(new MyMethodInterceptor());// 4.创建代理对象UserService userServiceProxy = (UserService) enhancer.create();// 通过代理对象调用目标方法userServiceProxy.addUser("张三");}
}

5.1.3 两种代理的区别

JDK 动态代理要求目标类必须实现接口。当创建一个代理对象时,JDK 会在运行时动态生成一个代理类,这个代理类实现了与目标类相同的接口。然后通过反射机制,在代理类中调用目标类的同名方法,并在调用前后添加切面逻辑,从而实现对目标方法的增强。

CGLIB 通过继承目标类来创建代理类,在代理类中重写目标类的方法,从而实现对目标方法的增强。

5.2 后置处理器相关

5.2.1 后置处理器

在 Spring AOP 的实现中,后置处理器(BeanPostProcessor)是一个关键的组件。它允许在 Spring 容器创建和初始化 Bean 的过程中,对 Bean 进行额外的处理。
具体到 AOP,这个后置处理器承担了重要职责,它负责识别哪些 Bean 需要进行 AOP 代理创建,以及如何创建和配置这些代理。

5.2.2 增强器(封装切面的信息)

抽取切面方法为增强器。
增强器是对切面中各种通知方法以及切点信息等的一种封装。它包含了要在何时(切点)、以何种方式(通知类型)对目标对象的哪些方法进行增强的所有信息。
抽取过程:Spring 会在启动时扫描所有配置的切面类,解析其中的切面方法和切点表达式等。例如,对于一个包含了前置通知和后置通知的切面类,Spring 会将这些通知方法以及它们所对应的切点信息提取出来,封装成一个个增强器。

5.2.3 在 getBean 时的创建流程

(比如在 Spring 容器启动后,当应用程序需要获取某个 Bean 实例时)Bean 创建时,会调用 getBean 方法。此时,AOP 的后置处理器就会判断要创建的这个Bean是否为AOP目标类、是否有切面需要对它增强。
如果是,就会创建代理类,并将增强器的信息保存到代理类内部。(这样,代理类就知道在哪些方法调用时需要应用哪些增强逻辑。)

6.设计模式

6.1 代理模式

代理模式
代理模式(Proxy pattern): 为另一个对象提供一个替身或占位符,以控制对这个对象的访问。
在这里插入图片描述

举个简单的例子:我(client)如果要买(doOperation)房,可以找中介(proxy)买房,中介直接和卖方(target)买房。中介和卖方都实现买卖(doOperation)的操作。中介就是代理(proxy)。

6.2 责任链模式(依次调用增强器的通知方法)

责任链模式的原理:责任链模式是一种行为设计模式,它将请求的发送者和接收者解耦,让多个对象都有机会处理请求,形成一个链条,直到请求被处理为止。

在 AOP 中的应用:当代理类的某个方法被调用时,它会根据保存的增强器信息(通常会使用一些数据结构来存储这些信息,例如,可能会使用一个列表来保存所有适用于该代理类的增强器,每个增强器在列表中都有其特定的顺序和位置,以确定其执行的先后顺序。),以责任链的方式依次调用各个增强器所对应的通知方法。例如,如果有一个前置通知增强器和一个后置通知增强器,那么在目标方法被调用前,会先执行前置通知增强器中的前置通知方法,然后调用目标方法,最后再执行后置通知增强器中的后置通知方法。

7.aop失效

7.1 aop失效的场景

7.1.1 同一个类内部中,不同方法间的调用

当在一个类的内部方法中调用另一个被 AOP 增强的本类方法时,AOP 切面可能不会生效。
这是因为 Spring AOP 默认是基于代理的。而类内部方法调用是直接调用目标方法,不会通过代理对象,所以无法触发 AOP 的增强逻辑。

@Component
public class MyService {public void methodA() {// 直接调用本类的methodB,AOP切面不会生效methodB(); }@Transactional // 假设这是一个事务切面,正常情况下应该开启事务public void methodB() {// 业务逻辑代码}
}

7.1.2 多线程环境下,线程直接调用目标对象方法

在多线程场景中,如果线程直接调用目标对象的方法,而不是通过代理对象调用,AOP 切面也会失效。因为每个线程都有自己的执行路径,若不通过代理,就无法触发 AOP 的拦截和增强机制。
通过代理对象调用方法才能aop,如果没有代理对象直接调用方法aop失效。

public class MyService {public void doSomething() {System.out.println("执行目标方法");}
}
public class Main {public static void main(String[] args) {MyService target = new MyService();// 直接调用目标对象的方法,未通过代理对象,AOP不会生效//创建一个新的线程对象,它定义了新线程要执行的任务内容。在这里,它表示新线程启动后要执行的操作就是调用target对象(前面创建的MyService实例)的doSomething方法。new Thread(() -> target.doSomething()).start(); // 通过代理对象调用,AOP会生效MyService proxy = (MyService) ProxyFactory.getProxy(target); new Thread(() -> proxy.doSomething()).start(); }//通过一个名为ProxyFactory的工厂类的getProxy方法来为前面创建的目标对象target创建一个代理对象。然后将创建好的代理对象强制转换为MyService类型,并赋值给变量proxy。//new Thread后定义了新线程要执行的任务内容。这次是告诉新线程启动后要执行的操作是调用proxy对象的doSomething方法。
}

AOP 的切面逻辑是添加在代理对象上的,只有通过代理对象调用方法,才能触发切面的增强操作。

7.1.3 final 修饰的方法不能被重写

如果一个类中的方法被声明为 final,那么 Spring AOP 将无法对其进行代理和增强,因为 final 方法不能被重写。而 AOP 的实现原理通常是基于动态代理生成子类或实现接口来实现增强的,这与 final 的特性相冲突。

JDK 代理是基于接口的,它主要关注的是接口中定义的方法,而接口中的方法默认是public abstract的,不存在final修饰的情况。所以从这个角度来说,JDK 代理不会直接受到目标类中final方法的影响,因为它根本不涉及对目标类方法的重写,而是在实现接口的代理类中去调用目标类的方法。
但是如果在接口的实现类中,有一些方法是final的,并且这些方法在接口中也有定义,那么实际上在代理类调用这些final方法时,也无法对其进行增强。

CGLIB 通过继承目标类来创建代理类。当使用 CGLIB 创建代理对象时,它会在运行时动态生成一个目标类的子类作为代理类。这个代理类会重写目标类中的所有非final的方法。

@Component
public class MyFinalService {// 该方法被声明为final,AOP切面无法生效public final void finalMethod() {// 业务逻辑代码}
}

7.1.4 private方法由于访问权限

private 表示私有的访问权限,是最严格的访问控制级别。被 private 修饰的成员变量和方法只能在当前类的内部被访问,其他类包括子类都无法直接访问。
在基于代理的 AOP 实现中,无法被外部的代理类访问和增强,所以 AOP 对其无效。

@Component
public class MyPrivateService {// 该方法为private,AOP切面无法生效private void privateMethod() {// 业务逻辑代码}

7.2 解决方法

参考aop失效的原因,对应解决


未写知识:切面的实现过程、AOP代理的创建

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com