Spring核心 AOP

1.什么是AOP?

AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。

比如,在《Spring实战(第4版)》中有如下一张图描述了AOP的大体模型。

在这里插入图片描述

可以看出:所谓切面,其实就相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块

总结:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。

2.实战案例

2.1.导入AOP依赖

在项目的pom.xml文件中引入AOP的依赖,如下所示:

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

Spring AOP对面向切面编程做了一些简化操作,只需要加上几个核心注解,AOP就能工作起来

2.2.定义目标类

在com.tianxia.springannotation.aop包下创建一个业务逻辑类,例如MathCalculator,用于处理数学计算上的一些逻辑。比如,我们在MathCalculator类中定义了一个除法操作,返回两个整数类型值相除之后的结果,如下所示

package com.tianxia.springannotation.aop;

/**
 * 业务逻辑类
 * @author liqb
 * @date 2023-05-09 16:30
 **/
public class MathCalculator {

    /**
     * 除法
     * @author liqb
     * @date 2023-05-09 16:31
     * @param i 除数
     * @param j 被除数
     * @return
     */
    public int div(int i, int j) {
        System.out.println("MathCalculator...div...");
        return i / j;
    }
}

需求:希望在业务逻辑运行的时候将日志进行打印,而且是在方法运行之前、方法运行结束、方法出现异常等等位置,都希望会有日志打印出来。

2.3.定义切面类

在com.tianxia.springannotation.aop包下创建一个切面类,例如LogAspects,在该切面类中定义几个打印日志的方法,以这些方法来动态地感知MathCalculator类中的div()方法的运行情况。如果需要切面类来动态地感知目标类方法的运行情况,那么就需要使用Spring AOP中的一系列通知方法了。

AOP中的通知方法及其对应的注解与含义如下:

  • 前置通知(对应的注解是@Before):在目标方法运行之前运行
  • 后置通知(对应的注解是@After):在目标方法运行结束之后运行,无论目标方法是正常结束还是异常结束都会执行
  • 返回通知(对应的注解是@AfterReturning):在目标方法正常返回之后运行
  • 异常通知(对应的注解是@AfterThrowing):在目标方法运行出现异常之后运行
  • 环绕通知(对应的注解是@Around):动态代理,我们可以直接手动推进目标方法运行(joinPoint.procced())

一开始的写法:

package com.tianxia.springannotation.aop;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Before;

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
public class LogAspects {

    // @Before:在目标方法(即div方法)运行之前切入
    @Before("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
    public void logStart() {
        System.out.println("除法运行......@Before,参数列表是:{}");
    }

    // 在目标方法(即div方法)结束时被调用
    @After("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
    public void logEnd() {
        System.out.println("除法结束......@After");
    }

    // 在目标方法(即div方法)正常返回了,有返回值,被调用
    @AfterReturning("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
    public void logReturn() {
        System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
    }

    // 在目标方法(即div方法)出现异常,被调用
    @AfterThrowing("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
    public void logException() {
        System.out.println("除法出现异常......异常信息:{}");
    }
}

对MathCalculator类中的div()方法进行切入,因此在每一个通知方法上都写了com.tianxia.springannotation.aop.MathCalculator.*(…)"这样一串玩意,其实它就是切入点表达式,即指定在哪个方法切入。

如果切入点表达式都一样的情况下,可以抽取出一个公共的切入点表达式,就像下面这样:

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
public class LogAspects {

    // 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
    @Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
    public void pointCut() {
        
    }
}

pointCut()方法就是抽取出来的一个公共的切入点表达式,其实该方法的方法名随便写啥都行,但是方法体中啥都别写

如何在每一个通知方法上引用这个公共的切入点表达式呢?

得分两种情况来讨论,第一种情况,如果是本类引用,那么可以像下面这样写:

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
public class LogAspects {

    // 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
    @Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
    public void pointCut() {

    }

    // @Before:在目标方法(即div方法)运行之前切入
    @Before("pointCut()")
    public void logStart() {
        System.out.println("除法运行......@Before,参数列表是:{}");
    }
}

第二种情况,如果是外部类(即其他的切面类)引用,那么就得在通知注解中写方法的全名了,例如:

package com.tianxia.springannotation.aop;

import org.aspectj.lang.annotation.*;

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
public class LogAspects {

    // 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
    @Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
    public void pointCut() {

    }

    // @Before:在目标方法(即div方法)运行之前切入
    @Before("com.tianxia.springannotation.aop.LogAspects.pointCut()")
    public void logStart() {
        System.out.println("除法运行......@Before,参数列表是:{}");
    }

    // 在目标方法(即div方法)结束时被调用
    @After("com.tianxia.springannotation.aop.LogAspects.pointCut()")
    public void logEnd() {
        System.out.println("除法结束......@After");
    }

    // 在目标方法(即div方法)正常返回了,有返回值,被调用
    @AfterReturning("com.tianxia.springannotation.aop.LogAspects.pointCut()")
    public void logReturn() {
        System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
    }

    // 在目标方法(即div方法)出现异常,被调用
    @AfterThrowing("com.tianxia.springannotation.aop.LogAspects.pointCut()")
    public void logException() {
        System.out.println("除法出现异常......异常信息:{}");
    }
}

必须告诉Spring哪个类是切面类,只需要给切面类上加上一个@Aspect注解即可

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
@Aspect
public class LogAspects {
}

2.4.将目标类和切面类加入到IOC容器

在com.tianxia.springannotation.config包中,新建配置类MainConfigOfAOP,并使用@Configuration注解标注这是一个Spring的配置类,同时使用@EnableAspectJAutoProxy注解开启基于注解的AOP模式。在MainConfigOfAOP配置类中,使用@Bean注解将业务逻辑类(目标方法所在类)和切面类都加入到IOC容器中,如下所示:

package com.tianxia.springannotation.config;

import com.tianxia.springannotation.aop.LogAspects;
import com.tianxia.springannotation.aop.MathCalculator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * AOP配置类
 * @author liqb
 * @date 2023-05-09 16:57
 **/
@Configuration
@EnableAspectJAutoProxy
public class MainConfigOfAOP {

    /**
     * 将业务逻辑类(目标方法所在类)加入到容器中
     * @author liqb
     * @date 2023-05-09 16:58
     * @return
     */
    @Bean
    public MathCalculator calculator() {
        return new MathCalculator();
    }

    /**
     * 将切面类加入到容器中
     * @author liqb
     * @date 2023-05-09 16:58
     * @return
     */
    @Bean
    public LogAspects logAspects() {
        return new LogAspects();
    }
}

2.5.测试

运行测试方法方法,输出的结果信息如下所示:

package com.tianxia.springannotation;

import com.tianxia.springannotation.aop.MathCalculator;
import com.tianxia.springannotation.config.MainConfigOfAOP;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * aop测试类
 * @author liqb
 * @date 2023-05-09 17:03
 **/
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AopTest {

    /**
     * 测试类
     * @author liqb
     * @date 2023-05-09 17:04
     */
    @Test
    public void test01() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);

        // 我们要使用Spring容器中的组件
        MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);
        mathCalculator.div(1, 1);

        // 关闭容器
        applicationContext.close();
    }
}

在这里插入图片描述

并打印出了相关信息,但是并没有打印参数列表和运行结果

要想打印出参数列表和运行结果,就需要对LogAspects切面类中的方法进行优化,优化后的结果如下所示:

package com.tianxia.springannotation.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;

import java.util.Arrays;

/**
 * 日志切面
 * @author liqb
 * @date 2023-05-09 16:33
 **/
@Aspect
public class LogAspects {

    // 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
    @Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
    public void pointCut() {

    }

    // @Before:在目标方法(即div方法)运行之前切入
    @Before("pointCut()")
    public void logStart(JoinPoint joinPoint) {
        // System.out.println("除法运行......@Before,参数列表是:{}");

        Object[] args = joinPoint.getArgs(); // 拿到参数列表,即目标方法运行需要的参数列表
        System.out.println(joinPoint.getSignature().getName() + "运行......@Before,参数列表是:{" + Arrays.asList(args) + "}");
    }

    // 在目标方法(即div方法)结束时被调用
    @After("com.tianxia.springannotation.aop.LogAspects.pointCut()")
    public void logEnd(JoinPoint joinPoint) {
        // System.out.println("除法结束......@After");

        System.out.println(joinPoint.getSignature().getName() + "结束......@After");
    }

    // 在目标方法(即div方法)正常返回了,有返回值,被调用
    @AfterReturning(value = "com.tianxia.springannotation.aop.LogAspects.pointCut()", returning="result") // returning来指定我们这个方法的参数谁来封装返回值
    public void logReturn(JoinPoint joinPoint, Object result) { // 一定要注意:JoinPoint这个参数要写,一定不能写到后面,它必须出现在参数列表的第一位,否则Spring也是无法识别的,就会报错
        // System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");

        System.out.println(joinPoint.getSignature().getName() + "正常返回......@AfterReturning,运行结果是:{" + result + "}");
    }

    // 在目标方法(即div方法)出现异常,被调用
    @AfterThrowing(value = "com.tianxia.springannotation.aop.LogAspects.pointCut()", throwing = "exception")
    public void logException(JoinPoint joinPoint, Exception exception) {
        System.out.println(joinPoint.getSignature().getName() + "出现异常......异常信息:{" + exception + "}");
    }
}

需要注意的是,JoinPoint参数一定要放在参数列表的第一位,否则Spring是无法识别的,那自然就会报错了

运行测试方法,输出结果如下所示:

在这里插入图片描述

3.小结

搭建AOP测试环境时,虽然步骤繁多,但是我们只要牢牢记住以下三点,就会无往而不利了

  1. 将切面类和业务逻辑组件(目标方法所在类)都加入到容器中,并且要告诉Spring哪个类是切面类(标注了@Aspect注解的那个类)
  2. 在切面类上的每个通知方法上标注通知注解,告诉Spring何时何地运行,当然最主要的是要写好切入点表达式,这个切入点表达式可以参照官方文档来写
  3. 开启基于注解的AOP模式,即加上@EnableAspectJAutoProxy注解,这是最关键的一点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值