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类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里

相关推荐
utmhikari1 小时前
【架构艺术】Go语言微服务monorepo的代码架构设计
后端·微服务·架构·golang·monorepo
蜡笔小新星2 小时前
Flask项目框架
开发语言·前端·经验分享·后端·python·学习·flask
计算机学姐2 小时前
基于Asp.net的驾校管理系统
vue.js·后端·mysql·sqlserver·c#·asp.net·.netcore
欢乐少年19044 小时前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥5 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
冰糖码奇朵6 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好6 小时前
【Spring】整合【SpringMVC】
java·spring
浪九天7 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
小斌的Debug日记8 小时前
框架基本知识总结 Day16
redis·spring
堕落年代8 小时前
Maven匹配机制和仓库库设置
java·maven