Spring AOP

23 篇文章 0 订阅


Spring AOP

1. 理解AOP

AOP即面向切面编程(Aspect Oriented Programming)

什么是面向切面编程?
image.png
切面指的是某一类特定的问题,所以AOP也可以理解为面向特定方法编程
比如对于网页登录,后端实现的登录拦截,就是一类特点问题
其中,登录拦截器就是对"登录校验"这类问题的统一处理
拦截器就是AOP思想的一种应用
同样的,统一数据返回格式和统一异常处理,也是AOP思想的实现
总的来说,AOP是一种思想,是对某一类问题的集中处理

举个例子:
image.png
假设这个项目的方法调用链比较复杂,现在有一些业务的执行效率很低,需要我们对耗时比较长的接口进行优化
首先就是确定出执行时间比较长的方法是哪些,就需要去统计每一个业务方法的执行耗时
可以在业务方法的执行前后记录时间,从而算出耗时
但是如果简单粗暴的去给每个方法添加计时代码,不是一件容易的工作
image.png

AOP就可以实现在不改动这些代码的基础上,针对特点的方法就进行功能的增强,即无侵入性
image.png

使用Spring AOP

我们针对图书管理系统的Controller进行耗时计算:
首先要AOP依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写程序

@Slf4j
@Aspect
@Component
public class TimeRecordAspect {
    @Around("execution(* org.jwcb.book.demo.controller.*.*(..))")
    public Object timeRecord(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        long end = System.currentTimeMillis();

        log.info(joinPoint.getSignature() + "耗时" + (end - start) + "ms");
        return result;
    }
}

执行后依次访问每个接口:
image.png
通过上面的例子,我们也能简单的感受到AOP的几个优势:

  1. 代码无侵入
  2. 减少了重复代码
  3. 提高了开发效率
  4. 维护方便

image.png

2. Spring AOP详解

2.1 Spring AOP中涉及的几个核心概念

切点(Pointcut)

Pointcut的作用就是提供一组规则,告诉程序对哪些方法来进行功能加强
image.png
这里的表达式execution(* org.jwcb.book.demo.controller.*.*(..))就是切点表达式

连接点(Join Point)

满足上一点所说的切点表达式规则的方法,就是切点
image.png

通知(Advice)

通知就是具体要做工作,指那些重复,共性的工作(最终体现为一个方法)
image.png

切面

切面 = 切点 + 通知
通过切面就能够知道当前AOP程序需要针对哪些方法,在什么时候执行什么样的操作
image.png
切面所在的类,称为切面类(被@Aspect标注的类)

2.2 通知类型

Spring的通知类型有以下几种:

  • @Around环绕通知,此注解标注的通知方法在目标方法前后都被执行
  • @Before前置通知,此注解标注的通知方法在目标方法前被执行
  • @After后置通知,此标注的通知方法在目标方法后被执行,且无论是否出现异常都会执行
  • @AfterReturning返回后通知,此注解标注的方法在目标方法执行后被执行,有异常不会执行
  • @AfterThrowing异常后通知,此注解标注的方法在发生异常后执行
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("Test1")
    public String Test1(){
        return "t1";
    }


    @RequestMapping("/Test2")
    public String Test2() {
        int a = 10 / 0;
        return "t2";
    }
}




@Slf4j
@Component
@Aspect
public class TestAspect {
    @Before("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }
    
    
    @After("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }
    
    
    @AfterReturning("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }
    
    
    @AfterThrowing("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }
    
    
    @Around("execution(* org.jwcb.je0803.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}


正常访问的情况:
image.png
此时@AfterThrowing 标识的通知不会执行
同时,@Around标识的通知方法包含前后两部分,前置的在@Before标识的通知方法前执行,后置的在@After标识的方法后执行

抛出异常情况:
image.png
此时@AfterReturning 标识的通知⽅法不会执行, @AfterThrowing 标识的通知⽅法执行了
并且@Around 环绕通知中原始方法法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了
image.png

2.2 @PointCut

Spring提供了@PointCut注解,将公共的切点表达式提取出来,需要时引用使用即可

@Slf4j
@Component
@Aspect
public class TestAspect {
    @Pointcut("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void pointcut() {}


    @Before("pointcut()")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }


    @After("pointcut()")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }


    @AfterReturning("pointcut()")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }


    @AfterThrowing("pointcut()")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }


    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}

2.3 切面优先级@Order

有时候我们在一个项目里面定义了多个切面类的时候,如果这些切面类的多个切点都匹配了同一个方法,那么执行的时候,顺序是什么样的??
测试一下:
新建类TestAspect1,TestAspect2,TestAspect3,内容与TestAspect上面的一样

@Slf4j
@Component
@Aspect
public class TestAspect3 {
    @Pointcut("execution(* org.jwcb.je0803.controller.*.*(..))")
    public void pointcut() {}


    @Before("pointcut()")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }


    @After("pointcut()")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }


    @AfterReturning("pointcut()")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }


    @AfterThrowing("pointcut()")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }


    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}

image.png
根据测试结果,可以知道,当存在多个切面类的时候,默认是按照类名字母的顺序排序
总体特点有点像栈先进后出的
但是,这种方式不太适合管理
image.png
Spring提供了一个注解@Order,可以用来控制通知的执行顺序
使用方式:
image.png
执行结果:
image.png
可以看出,根据我们定义的优先级,数组越小的先执行
image.png

2.4 切点表达式

execution

我们上面使用的execution()是最常用的切点表达式,语法为:
image.png

  1. * 表示匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
    1. 包名使用*表示任意包(一层包用一个*)
    2. 类名使用*表示任意类
    3. 返回值使用*表示任意返回类型
    4. 方法名使用*表示任意方法
    5. 参数使用*表示一个任意类型的参数
  2. ..表示匹配多个连续的任意符号,可以调配任意层级的包,或任意类型,任意个数的参数
    1. 使用..配置包名,表示次包下的所有子包
    2. 使用..配置方法参数,表示任意个任意类型的参数

注意:访问修饰符和异常是可以省略的

@annotation

execution适用于有规则的,当时当我们要匹配多个之间无规则的方法,使用execution就不方便了
我们可以借助自定义注解的方式以及另一种切点表达式@annotation来描述这一类的切点
自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeCost {
}

@Target表示这个注解可以用在什么地方

  • TYPE用于描述类,接口(包括注解类型)或enum
  • METHOD:描述方法
  • PARAMETER:描述参数
  • TYPE_USE:可以标注任意类型

@Retention指注解被保留的时间长短,标明注解的生命周期

  • SOURCE:表示注解仅存在于源代码中,编译成字节码之后会被丢弃
  • CLASS:编译时注解,表示注解存在于源代码和字节码中,但是运行时会被丢弃
  • RUNTIME:运行时注解,表示注解存在于源代码,字节码和运行时中

定义切面类
使用@annotation切点表达式切点

@Slf4j
@Aspect
@Component
public class CostTimeAspect {
    @Around("@annotation(org.jwcb.je0803.aspect.TimeCost)")
    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature().toLongString()+"cost time: {} ms", end - start);
        return result;
    }
}

这样的话,如果我们需要计算某一个方法的执行耗时,只需要将自定义的注解加在方法上面即可

@RequestMapping("/test")
@RestController
public class TestController {
    @TimeCost
    @RequestMapping("Test1")
    public String Test1(){
        return "t1";
    }

}

访问:
image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值