SpringBoot 之 AOP

前言:

        Spring 三大核心思想是啥,还记得不?IOC(控制反转),DI(依赖注入),AOP(面向切面编程)。回顾一下这三个东西:

        IOC:不考虑使用 Spring,纯手写 java 程序时,对象之间的使用和依赖完全是由程序员手写代码控制的,何时创建对象完全在代码中标明。IOC 是一种编程思想,它的核心作用就是把对象的创建、管理由人工负责转成 Spring 托管。

        DI:IOC的核心思想很好,那么怎么实现呢?DI就是实现IOC的方案,例如常见的 @Value、@Resource、@Autowired 注解、配置文件都是最具体的使用样例。

        AOP:简单点说就是,把某个核心方法切开看看能不能加点其他处理逻辑。

一、相关注解

@Aspect 描述这是一个切面类
@Pointcut 定义一个切面,所关注事件入口
@Before 切入方法执行前干什么
@Around 切入方法执行时的增强处理,慎用
@After 切入方法执行后后干什么
@AfterReturning 对切入方法返回数据的增强处理
@AfterThrowing 切入方法抛出异常时的处理

二、常用注解说明

常用注解三个:Before、After、Pointcut

切面类:

@Slf4j
@Aspect
@Component
public class AopTest {

    @Pointcut("execution(* quancheng.demo.service..TestService.test(..))")
    public void pointFun() {
    }

    @Before("pointFun()")
    public void doB() {
        log.info("Before doB...");
    }

    @After("pointFun()")
    public void doA() {
        log.info("After doA...");
    }

}

切入的类和方法:

@Slf4j
@Service
public class TestService {

    public String test(String str) {
        log.info("test service......{}", str);
        return str;
    }
    
}

运行时日志截图:

PointCut 定义了一个切面,该注解有两个表达式:

1. execution 表达式

execution(* quancheng.demo.service..TestService.test(..))

* :表示返回值类型,* 表示所有类型;

quancheng.demo.service:这是一个包名,标识需要拦截的包;

包名后的 ..:表示当前包和所有其子包,在本例中指 quancheng.demo.service 包和其子包下所有类;

TestService :表示具体的类名,如果是 * 表示所有类;

test(..) :test是具体方法名,可以用 * 表示所有的方法;后面括弧里面表示方法的参数,双点表示任何参数;

2.@annotation 表达式(推荐使用

@annotation(quancheng.demo.aop.anno.ServiceDeal)

@annotation 的参数是一个注解的全类限定名,可以自定义也可以使用现有的注解;
quancheng.demo.aop.anno.ServiceDeal 是自定义的注解;

切面类样例如下:

@Slf4j
@Aspect
@Component
public class AopTest {

    @Pointcut("@annotation(quancheng.demo.aop.anno.ServiceDeal)")
    public void pointFan() {
    }

    @Before("pointFan()")
    public void doB() {
        log.info("Before doB...");
    }

    @After("pointFan()")
    public void doA() {
        log.info("After doA...");
    }
}

切面类的自定义注解:

@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceDeal {
    String value() default "";
}

使用样例:

@Slf4j
@Service
public class TestService {

    @ServiceDeal
    public String test1(String str) {
        log.info("test1 service......{}", str);
        return str;
    }
}

三、增强处理注解

增强处理注解:AfterReturning、AfterThrowing、Around

1.AfterReturning

这个只能加些处理逻辑,对修改返回无能为力。

@Slf4j
@Aspect
@Component
public class AopTest {

    @Pointcut("@annotation(quancheng.demo.aop.anno.ServiceDeal)")
    public void pointFan() {
    }

    @Before("pointFan()")
    public void doB() {
        log.info("Before doB...");
    }

    @After("pointFan()")
    public void doA() {
        log.info("After doA...");
    }

    @AfterReturning(pointcut = "pointFan()", returning = "result")
    public void dealReturn(String result) {
        log.info("打印返回值:{}", result);
    }

}

输出截图:

注意:returning 的值要跟参数的属性保持一致,要不会报错。

2.AfterThrowing

service 加个会抛出异常的方法:

    @ServiceDeal
    public String test1(String str) {
        Integer i = Integer.valueOf(str);
        log.info("test1 service......{}", str);
        return str;
    }

切面方法处理异常:

    @AfterThrowing(pointcut = "pointFan()", throwing = "e")
    public void dealException(Throwable e) {
        log.info("打印报错:{}", e.getStackTrace());
    }

输出截图:

 3.Around

        当定义一个 Around 增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型。在增强处理方法体内,调用 ProceedingJoinPoint 的 proceed 方法才会执行目标方法;调用 ProceedingJoinPoint 的 proceed 方法时,还可以传入一个 Object[] 对象,该数组中的值将被传入目标方法作为实参,这就是 Around 增强处理方法可以改变目标方法参数值的关键,当然传参类型和数量是必须跟实际方法一致。

这里完全重新写一个注解实例:

1)定义注解,作用是为了校验age参数是否合法:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AgeCheck {
    String message() default "数据格式错误";

    int min() default 1;

    int max() default 100;

}

2)使用注解:

@Slf4j
@Service
public class TestService {

    @AgeCheck(min = 18, max = 65,message = "年龄范围错误")
    public String test(Integer age) {
        return age + "";
    }

}

3)定义切面处理逻辑:

@Slf4j
@Aspect
@Component
public class AgeCheckAop {

    @Pointcut("@annotation(quancheng.demo.aop.anno.AgeCheck)")
    public void pointFan() {
    }

    @Before("pointFan()")
    public void doB() {
        log.info("Before doB...");
    }

    @After("pointFan()")
    public void doA() {
        log.info("After doA...");
    }

    @Around(value = "pointFan()")
    public Object process(ProceedingJoinPoint joinPoint) {
        log.info("进入Around,此时未执行函数方法");
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        AgeCheck ageCheck = methodSignature.getMethod().getAnnotation(AgeCheck.class);
        Integer min = ageCheck.min();
        Integer max = ageCheck.max();
        String message = ageCheck.message();
        log.info("min:{},max:{}", min, max);

        Object[] args = joinPoint.getArgs();
        log.info("原始传参 args:{}", args);
        Integer age = (Integer) args[0];
        if (age < min || age > max) {
            log.info("不满足条件直接返回,message:{}", message);
            return message;
        }
        Object result = null;
        try {
            //可以在 proceed() 方法里面修改传参
            result = joinPoint.proceed(args);
            log.info("在Around,此时已执行完函数方法");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        log.info("数据返回:{},这块可以加些处理逻辑,甚至修改返回", result);
        return result;
    }

}

输入age=19时(此时满足校验逻辑,程序会一直执行到最后),截图:

 输入age=17时(此时不满足校验逻辑,程序会提前返回),截图:

传参和结果都可随意修改,所以这块慎用。

你可能感兴趣的:(后端,spring,boot,aop,切面)