Spring全家桶(三):Spring AOP

Spring AOP面向切面编程

1.面向切面编程思维(AOP)

1.1.面向切面编程思想AOP

AOP:Aspect Oriented Programming面向切面编程

AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用AOP,可以在不修改原来代码的基础上添加新功能。

1.2AOP思想主要的应用场景

AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以应用于各种场景,以下是一些常见的AOP应用场景:

  1. 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。

  2. 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。

  3. 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。

  4. 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。

  5. 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。

  6. 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。

  7. 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。 综上所述,AOP可以应用于各种场景,它的作用是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。

1.1.3AOP术语名词介绍

1-横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

2-通知(增强)

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法前执行

  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝

  • 异常通知:在被代理的目标方法异常结束后执行(死于非命

  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论

  • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

4-切入点 pointcut

定位连接点的方式,或者可以理解成被选中的连接点!

是一个表达式,比如execution(* com.spring.service.impl..(..))。符合条件的每个方法都是一个具体的连接点。

5-切面 aspect

切入点和通知的结合。是一个类。

6-目标 target

被代理的目标对象。

7-代理 proxy

向目标对象应用通知之后创建的代理对象。

8-织入 weave

指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。

2. Spring AOP框架介绍和关系梳理

  1. AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!

  2. 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐!

  3. Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架!SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!

3. Spring AOP基于注解方式实现和细节

3.1 Spring AOP底层技术组成
  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。

  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。

  • AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。

3.2 初步实现

1.加入依赖

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.6</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
</dependency>

2.准备接口

public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
    
}

3.纯净实现类

/**
 * 实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能!
 */
@Component
public class CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        return result;
    }
}
```

4.声明切面类

package com.atguigu.advice;

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

// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
        
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogBeforeCore() {
        System.out.println("[AOP前置通知] 方法开始了");
    }
    
    @AfterReturning(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogAfterSuccess() {
        System.out.println("[AOP返回通知] 方法成功返回了");
    }
    
    @AfterThrowing(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogAfterException() {
        System.out.println("[AOP异常通知] 方法抛异常了");
    }
    
    @After(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogFinallyEnd() {
        System.out.println("[AOP后置通知] 方法最终结束了");
    }
    
}
3.3 获取通知细节信息

1.JointPoint接口

需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

  • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)

  • 要点2:通过目标方法签名对象获取方法名

  • 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组

    // @Before注解标记前置通知方法
    // value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
    // 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
    // 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
    @Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogBeforeCore(JoinPoint joinPoint) {

      // 1.通过JoinPoint对象获取目标方法签名对象
      // 方法的签名:一个方法的全部声明信息
      Signature signature = joinPoint.getSignature();
      
      // 2.通过方法的签名对象获取目标方法的详细信息
      String methodName = signature.getName();
      System.out.println("methodName = " + methodName);
      
      int modifiers = signature.getModifiers();
      System.out.println("modifiers = " + modifiers);
      
      String declaringTypeName = signature.getDeclaringTypeName();
      System.out.println("declaringTypeName = " + declaringTypeName);
      
      // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
      Object[] args = joinPoint.getArgs();
      
      // 4.由于数组直接打印看不到具体数据,所以转换为List集合
      List<Object> argList = Arrays.asList(args);
      
      System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
    

    }

2.方法返回值

在返回通知中,通过 @AfterReturning注解的returning属性获取目标方法的返回值!

// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
        value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
        returning = "targetMethodReturnValue"
)
public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
    
    String methodName = joinPoint.getSignature().getName();
    
    System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
}

3.异常对象捕捉

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
        value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
        throwing = "targetMethodException"
)
public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
    
    String methodName = joinPoint.getSignature().getName();
    
    System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}

3.4 切点表达式语法

  1. 切点表达式作用

    AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。

  2. 切点表达式案例

    1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
    2.查询某包下类中第一个参数是String的方法
    3.查询全部包下,无参数的方法!
    4.查询com包下,以int参数类型结尾的方法
    5.查询指定包下,Service开头类的私有返回值int的无参数方法

4. Spring AOP基于XML方式实现(了解)

  1. 准备工作

    加入依赖

    和基于注解的 AOP 时一样。

    准备代码

    把测试基于注解功能时的Java类复制到新module中,去除所有注解。

  2. 配置Spring配置文件

    <bean id="calculatorPure" class="com.atguigu.aop.imp.CalculatorPureImpl"/> <bean id="logAspect" class="com.atguigu.aop.aspect.LogAspect"/>

    aop:config

     <!-- 配置切入点表达式 -->
     <aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
     
     <!-- aop:aspect标签:配置切面 -->
     <!-- ref属性:关联切面类的bean -->
     <aop:aspect ref="logAspect">
         <!-- aop:before标签:配置前置通知 -->
         <!-- method属性:指定前置通知的方法名 -->
         <!-- pointcut-ref属性:引用切入点表达式 -->
         <aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
     
         <!-- aop:after-returning标签:配置返回通知 -->
         <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
         <aop:after-returning
                 method="printLogAfterCoreSuccess"
                 pointcut-ref="logPointCut"
                 returning="targetMethodReturnValue"/>
     
         <!-- aop:after-throwing标签:配置异常通知 -->
         <!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
         <aop:after-throwing
                 method="printLogAfterCoreException"
                 pointcut-ref="logPointCut"
                 throwing="targetMethodException"/>
     
         <!-- aop:after标签:配置后置通知 -->
         <aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
     
         <!-- aop:around标签:配置环绕通知 -->
         <!--<aop:around method="......" pointcut-ref="logPointCut"/>-->
     </aop:aspect>
    

    </aop:config>

3.测试

@SpringJUnitConfig(locations = "classpath:spring-aop.xml")
public class AopTest {

    @Autowired
    private Calculator calculator;

    @Test
    public void testCalculator(){
        System.out.println(calculator);
        calculator.add(1,1);
    }
}

5. Spring AOP对获取Bean的影响理解

5.1 根据类型装配 bean
  1. 情景一

    • bean 对应的类没有实现任何接口

    • 根据 bean 本身的类型获取 bean

      • 测试:IOC容器中同类型的 bean 只有一个

        正常获取到 IOC 容器中的那个 bean 对象

      • 测试:IOC 容器中同类型的 bean 有多个

        会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

  2. 情景二

    • bean 对应的类实现了接口,这个接口也只有这一个实现类

      • 测试:根据接口类型获取 bean

      • 测试:根据类获取 bean

      • 结论:上面两种情况其实都能够正常获取到 bean,而且是同一个对象

  3. 情景三

    • 声明一个接口

    • 接口有多个实现类

    • 接口所有实现类都放入 IOC 容器

      • 测试:根据接口类型获取 bean

        会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个

      • 测试:根据类获取bean

        正常

  4. 情景四

    • 声明一个接口

    • 接口有一个实现类

    • 创建一个切面类,对上面接口的实现类应用通知

      • 测试:根据接口类型获取bean

        正常

      • 测试:根据类获取bean

        无法获取 原因分析:

    • 应用了切面后,真正放在IOC容器中的是代理类的对象

    • 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

  5. 情景五

    • 声明一个类

    • 创建一个切面类,对上面的类应用通知

      • 测试:根据类获取 bean,能获取到 debug查看实际类型:
5.2 使用总结

对实现了接口的类应用切面

对没实现接口的类应用切面new

如果使用AOP技术,目标类有接口,必须使用接口类型接收IoC容器中代理组件!

6.通知类型

AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置

AOP通知共分为5种类型

前置通知:在切入点方法执行之前执行

最终通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,最终通知都会执行。

环绕通知(重点):**手动调用切入点方法并对其进行增强的通知方式。

后置通知(了解):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行。

异常通知(了解):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行。

6.1、前置通知
  • 名称:@Before

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

  • 范例:

    @Before("pt()")
    public void before() {
    System.out.println("before advice ...");
    }

6.2、 最终通知
  • 名称:@After

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行

  • 范例:

    @After("pt()")
    public void after() {
    System.out.println("after advice ...");
    }

6.3、后置通知
  • 名称:@AfterReturning(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行

  • 范例:

    @AfterReturning("pt()")
    public void afterReturning() {
    System.out.println("afterReturning advice ...");
    }

6.4、 异常通知
  • 名称:@AfterThrowing(了解)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行

  • 范例:

    @AfterThrowing("pt()")
    public void afterThrowing() {
    System.out.println("afterThrowing advice ...");
    }

6.5、 环绕通知
  • 名称:@Around(重点,常用)

  • 类型:方法注解

  • 位置:通知方法定义上方

  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行

  • 范例:

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("around before advice ...");
    Object ret = pjp.proceed();
    System.out.println("around after advice ...");
    return ret;
    }

环绕通知注意事项:

环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。

环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里

相关推荐
九圣残炎3 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge5 分钟前
Netty篇(入门编程)
java·linux·服务器
LunarCod11 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
成富25 分钟前
文本转SQL(Text-to-SQL),场景介绍与 Spring AI 实现
数据库·人工智能·sql·spring·oracle
Re.不晚33 分钟前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐39 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。42 分钟前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航1 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself1 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言