Spring AOP(上)

1. AOP概述

        学习完Spring的统⼀功能之后, 我们进⼊到AOP的学习. AOP是Spring框架的第⼆⼤核⼼(第⼀⼤核⼼是 IoC)

1.1 什么是AOP

        Aspect Oriented Programming(⾯向切⾯编程)

        什么是⾯向切⾯编程呢?

        切⾯就是指某⼀类特定问题, 所以AOP也可以理解为⾯向特定⽅法编程.

        什么是⾯向特定⽅法编程呢?

        ⽐如上个章节学习的"登录校验", 就是⼀类特定问题. 登录校验拦截器, 就 是对"登录校验"这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应⽤.

        AOP是⼀种思想, 拦截器是AOP 思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接⼝. 同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现. 简单来说: AOP是⼀种思想, 是对某⼀类事情的集中处理.

1.2 什么是Spring AOP

        AOP是⼀种思想, 它的实现⽅法有很多, 有Spring AOP, Spring AOP是其中的⼀种实现⽅式. 学会了统⼀功能之后, 并不是学会了Spring AOP;拦截器作⽤的维度是URL(⼀次请求和响应), @ControllerAdvice 应⽤场景主要是全局异常处理 (配合⾃定义异常效果更佳), 数据绑定, 数据预处理. AOP作⽤的维度更加细致(可以根据包、类、⽅法 名、参数等进⾏拦截), 能够实现更加复杂的业务逻辑.

          举个例子:我们现在有一个项目,项目中开发了很多功能。如图:

         现在有一些业务的执行效率比较低,耗时较长,我们需要对接口进行优化。

        第一步就需要定位出执行耗时比较长的业务方法,再针对该业务方法来进行优化。所以我们就需要统计当前项目中每一个业务方法的执行耗时。我们就需要在业务方法运行前和运行后,记录下方法的开始时间和结束时间,两者之差就是这个方法的耗时。如图:

        上面这种方法虽然可以解决问题,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法,如果我们要在每个业务方法中都记录方法的耗时,对于程序员而言,会增加很多的工作量。

        而 AOP 就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强AOP的作用:程序运行期间,在不修改源代码的基础上对已有方法进行增强 ——> 无入侵性:解耦)。

 2. Spring AOP快速⼊⻔

        学习什么是AOP后, 我们先通过下⾯的程序体验AOP的开发, 并掌握Spring中AOP的开发步骤.         需求: 统计图书系统各个接⼝⽅法的执⾏时间.

2.1 引入AOP依赖

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

2.2 编写AOP程序

@Slf4j
@Component
@Aspect
public class TimeRecordAspect {

    @Around("execution(* com.example.book_manage_240827.controller.*.*(..))")
    public Object record(ProceedingJoinPoint pj) throws Throwable {
        /**
         * 切面逻辑
         * 1. 记录开始时间
         * 2. 执行目标方法
         * 3. 记录结束时间
         * 4. 记录消耗时间
         */
        long start = System.currentTimeMillis();
        Object result = pj.proceed();
        //切面可能会执行在任何方法上,所以用object接受
        long end = System.currentTimeMillis();
        log.info("耗时时间: " + (end - start) + "ms");
        return result;

    }
}

        运行登录页面,观察日志:

        对程序进行简单的讲解:

1、@Aspect标识这是一个切面类

2、@Around环绕通知,在目标方法的前后都会被执行。后面的表达式表示对哪些方法进行增强。

3、ProceedingJoinPoint.proceed() 让原始方法执行。

        上述aop代码可以分为三个部分; 

         通过上面的程序,我们可以感受到AOP面向变成的一些优势:

1、代码无侵入:不修改原始的业务方法,就可以对原始的业务方法进行了功能的增强或者是功能的改变。

2、减少了重复代码

3、提高开发效率

4、方便维护

3. Spring AOP 详解

3.1 Spring AOP的核心概念

3.1.1 切点(Pointcut)

        也称之为 “切入点”,Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述),告诉程序对哪些方法来进行功能增强。

        around里面的execution(* com.example.book_manage_240827.controller.*.*(..))表达式就是切点表达式;

3.1.2 连接点(Join Point)

        满足切点表达式规则的方法,就是连接点。也就是可以被AOP控制的方法。

        以入门程序举例,所有 com.example.book_manage_240827.controller 路径下的方法,都是连接点。

3.1.3 通知(Advice)

        通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法),比如上述程序中记录业务方法的耗时时间,就是通知。

          在AOP当中,我们把这部分重复的代码逻辑取出来单独定义,这部分代码就是通知的内容。

3.1.4 切面(Aspect)

        切面(Aspect)= 切点(Pointcut) + 通知(Advice)。

          切面所在的类,我们一般称为切面类(被@Aspect注解表示的类)。

        通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作

3.1.5 总结

        切点就是一规则,连接点就是符合这个规则的对象,通知就是对象做的事情,切面就是符合规则的所有对象做事。

 eg:我要去上海嘉兴路去星梦剧院看演出;

切点:我。

连接点:有门票的,能去上海的我。

通知:在 2024-09-1 进行剧场演出。

切面:我要去上海嘉兴路去星梦剧院看演出;

3.2 通知类型

        上面我们讲了什么是通知,接下来学习通知的类型。@Around 就是其中一种通知类型,表示环绕通知。Spring 中 AOP 的通知类型有以下几种:

@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

@Before:前置通知,此注解标注的通知方法在目标方法前被执行。

@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。

@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。

   通知类代码如下:

@Slf4j
@Aspect
@Component
public class AspectDemo {
    //前置通知
    @Before("execution(* com.example.springaop.controller.*.*(..))")
    public void doBefore() {
        log.info("do before");
    }
    //后置通知
    @After("execution(* com.example.springaop.controller.*.*(..))")
    public void doAfter() {
        log.info("do after");
    }
    //添加环绕通知
    @Around("execution(* com.example.springaop.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("do around before");
        Object result = joinPoint.proceed();
        log.info("do around after");
        return result;
    }
    //返回后通知
    @AfterReturning("execution(* com.example.springaop.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("do after returning");
    }
    //抛出异常后通知
    @AfterThrowing("execution(* com.example.springaop.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("do after throwing");
    }
}

          测试接口如下:

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法...");
        return "t1";
    }
    @RequestMapping("/t2")
    public Boolean t2() {
        int a = 10 / 0;
        return true;
    }
}

3.2.1 正常运行的情况

        接口:http://127.0.0.1:8084/test/t1 ,测试结果如下:

        日志如下:

        执行的顺序是 Around Before -> Before -> AfterReturning -> After -> Around After,如图:

        程序正常运行的情况下,AfterThrowing 标识的通知方法不会执行。

3.2.2 运行失败的情况

        接口:http://127.0.0.1:8084/test/t2 ,测试结果如下:

         执行顺序:Around Before -> Before -> AfterThrowing-> After,如图:

         可以看到,因为有异常,所以会执行 AfterThrowing,没有执行 AfterReturning,改成了 AfterThrowing。而 After 无论是否有异常都会执行。因为有异常,所以执行完目标方法后,就不会执行 AroundAfter 了

3.2.3 @Around 通知类型

        其中单独测试一下 @Around 通知类型,有返回Object类型 和 没有返回Object类型的区别,代码如下:

        无返回Object类型:

    @Around("execution(* com.example.springaop.controller.*.*(..))")
    public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("do around before");
        log.info("do around after");
    }

         有返回Object类型:

    @Around("execution(* com.example.springaop.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("do around before");
        Object result = joinPoint.proceed();
        log.info("do around after");
        return result;
    }

        无返回类型 和 有返回类型的区别如下所示:

          要注意,@Around 环绕通知方法的返回值,必须指定为 Object 类型

3.2.4  注意事项

1、@Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。

2、@Around 环绕通知方法的返回值,必须指定为Object 类型,来接收原始方法的返回值,否则原始方法执行完毕,使获取不到返回值的。

3、一个切面类可以有多个切点。

 3.3 @PointCut

        上面代码存在一个问题,就是存在大量重复的切点表达式:execution(* com.example.springaop.controller.*.*(..)),Spring 提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用到时引入该切入点表达式即可。

        上面代码就可以修改为:

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectDemo {

    @Pointcut("execution(* com.example.aop.controller.*.*(..))")
    private void pt(){};
    //public void pt(){} 其他类要使用pt()时,使用public修饰
    //前置通知
    @Before("pt()")
    public void doBefore() {
        log.info("执⾏ Before ⽅法");
    }
    //后置通知
    @After("pt()")
    public void doAfter() {
        log.info("执⾏ After ⽅法");
    }
    //返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        log.info("执⾏ AfterReturning ⽅法");
    }
    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        log.info("执⾏ doAfterThrowing ⽅法");
    }
    //添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("Around ⽅法开始执⾏");
        Object result = joinPoint.proceed();
        log.info("Around ⽅法结束执⾏");
        return result;
    }
}

        当切点定义使用 private 修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把 private 改为 public。引用方式:全限定类名.方法名()。

        当其他切面类使用上面这个PointCut时,注释代码如下:

@Slf4j
@Aspect
@Component
public class AspectDemo2 {
    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2 -> Before ⽅法");
    }
}

        测试接口:http://127.0.0.1:8084/test/t1,日志如下:

3.4 切面优先级 @Order

        当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?

        不同类的通知运行的顺序是怎么样的呢?现在切面类有下面几个:

@Slf4j
@Component
@Aspect
public class AspectDemo2 {
    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore(){
        log.info("执行AspectDemo2.before方法...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter(){
        log.info("执行AspectDemo2.after方法...");
    }
}
@Slf4j
@Component
@Aspect
public class AspectDemo3 {
    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore(){
        log.info("执行AspectDemo3.before方法...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter(){
        log.info("执行AspectDemo3.after方法...");
    }
}
@Slf4j
@Component
@Aspect
public class AspectDemo4 {
    @Before("com.example.aop.aspect.AspectDemo.pt()")
    public void doBefore(){
        log.info("执行AspectDemo4.before方法...");
    }

    @After("com.example.aop.aspect.AspectDemo.pt()")
    public void doAfter(){
        log.info("执行AspectDemo4.after方法...");
    }
}

         测试顺序如下:

         可以看到默认给我们的排序是按切面类名大小进行排序的。存在多个切面类时,默认按照切面类的类名字母排序

@Before 通知:字母排名靠前的先执行。

@After 通知:字母排名靠前的后执行。

        也可以按照自定义顺序,下面是@Order注解的使用(@Order注解 的括号里面放数字) ;

@Slf4j
@Aspect
@Order(3)
@Component
public class AspectDemo2 {
    @Before("com.example.springaop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo2 -> Before ⽅法");
    }
 
    @After("com.example.springaop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo2 -> After ⽅法");
    }
}
 
@Slf4j
@Aspect
@Order(2)
@Component
public class AspectDemo3 {
    @Before("com.example.springaop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo3 -> Before ⽅法");
    }
 
    @After("com.example.springaop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo3 -> After ⽅法");
    }
}
 
@Slf4j
@Aspect
@Order(1)
@Component
public class AspectDemo4 {
    @Before("com.example.springaop.aspect.AspectDemo.pt()")
    public void doBefore() {
        log.info("执⾏ AspectDemo4 -> Before ⽅法");
    }
 
    @After("com.example.springaop.aspect.AspectDemo.pt()")
    public void doAfter() {
        log.info("执⾏ AspectDemo4 -> After ⽅法");
    }
}

        运行观察日志,如图:

        通过上述程序的运行结果,得出结论,@Order 注解标识的切面类,执行顺序如下:        

        @Before 通知:数字小 的 先执行。

        @After 通知:数字大 的先执行。

5. 切点表达式

        上面的代码中,我们一直在使用切点表达式来描述切点,下面进行介绍切点表达式的语法,切点表达式常见有两种表达方式:

1、execution(......):根据方法的签名来匹配。

2、@annotation(......):根据注解匹配。

5.1 execution 表达式

        execution() 是最常用的切点表达式,用来匹配方法,语法为:

execution( <访问修饰符>  <返回类型>  <包名.类名.方法(方法参数)> <异常> )

        上面的切点表达式意思是:任意的返回类型,在....controller包下的任意类方法(任意方法参数都行),其中:访问修饰符和异常可以省略

         切点表达式支持通配符:

1、* :匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)

a、包名使用 * 表示任意包(一层包使用一个 *)。

b、类名使用 * 表示任意类。

c、返回值使用 * 表示任意返回值类型。

2、.. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数。

a、使用 .. 配置包名,标识此包以及此包下的所有子包。

b、可以使用 .. 配置参数,任意个任意类型的参数。

d、方法名使用 * 表示任意方法。

e、参数使用 * 表示一个任意类型的参数。

        切点表达式:

TestController 下的 public 修饰,返回类型为String 、方法名为t1、无参方法:

execution(public String com.example.demo.controller.TestController.t1())

省略访问修饰符:

execution(String com.example.demo.controller.TestController.t1())

匹配所有返回类型:

execution(* com.example.demo.controller.TestController.t1())

匹配TestController 下的所有⽆参方法:

execution(* com.example.demo.controller.TestController.*())

 匹配TestController 下的所有方法:

execution(* com.example.demo.controller.TestController.*(..))

匹配controller包下所有的类的所有方法:

execution(* com.example.demo.controller.*.*(..))

匹配所有包下⾯的TestController:

execution(* com..TestController.*(..))

匹配com.example.demo包下,子孙包下的所有类的所有⽅法:

execution(* com.example.demo..*(..))

5.2 @annotation

        execution 表达式更适用有规则的,如果我们要匹配多个无规则的方法呢?比如:TestController 中的 t1() 和 UserController 中的 u1() 这两个方法。这个时候我们使用 execution 这种 切点表达式来描述就很不方便了。

        此时我们就可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点。

        实现步骤:

1、编写自定义注解。

2、使用 @annotation 表达式来描述切点。

3、在连接点的方法上添加自定义注解。

        准备测试代码:

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法...");
        return "t1";
    }
    @RequestMapping("/t2")
    public Boolean t2() {
        log.info("执行t2方法...");
        return true;
    }
}
 
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
 
    @RequestMapping("/u1")
    public String u1() {
        log.info("执行u1方法...");
        return "u1";
    }
 
    @RequestMapping("/u2")
    public String u2() {
        log.info("执行u2方法...");
        return "u2";
    }
}

 5.2.1 自定义注解 @MyAspect

        创建一个注解类(和创建 Class 文件一样的流程,选择 Annotation 就可以了):

添加两个注解之后的代码如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

1、@Target 标识了 Annotation 所修饰的对象范围,即该注解可以用在什么地方

          常用取值:

        ElementType.TYPE:用于描述类、接口(包括注解类型) 或 enum声明。
        ElementType.METHOD:描述方法。

        ElementType.PARAMETER:描述参数。

        ElementType.TYPE_USE:可以标注任意类型。
2、@Retention 指 Annotation 被保留的时间长短,标明注解的生命周期。

        ①RentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运⾏时⽆法获取到该注解的信息,只能在编译时使用。比如@SuppressWarnings ,以及lombok提供的注解 @Data ,@Slf4j。

        ②RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运⾏时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时⽆法获取。通常⽤于⼀些框架和⼯具的注解。

        ③RetentionPolicy.RUNTIME:运⾏时注解。表示注解存在于源代码,字节码和运⾏时中。这意味着在编译时,字节码中和实际运⾏时都可以通过反射获取到该注解的信息。通常用于⼀些需要在运⾏时处理的注解,如Spring的 @Controller  @ResponseBody。

5.2.2 切面类

        使用 @annotation 切点表达式定义切点,只对 @MyAspect生效,代码如下:

@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    @Around("@annotation(com.example.aop.MyAspect)")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        log.info("do around before");
        Object o = null;
        try {
            o = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("发生异常, e:" + e);
        }
        log.info("do around after");
        return o;
    }
}

        上面的通知类型使用 @annotation注解后,注解括号里的 MyAspect,表示谁使用 @MyAspect注解,就会执行下面代码。

5.2.3 添加自定义注解

        在 TestController 中的 t1() 和 UserController 中的 u1 这两个方法上添加自定义注解 @MyAspect,其他方法不添加。(已把之前的AspectDemo的@Comment注解给注释掉了);

@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @MyAspect
    @RequestMapping("/u1")
    public String u1() {
        log.info("执行u1方法...");
        return "u1";
    }
}
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    @MyAspect
    @RequestMapping("/t1")
    public String t1() {
        log.info("执行t1方法...");
        return "t1";
    }
}

        运行程序,测试接口:127.0.0.1:8084/test/t1;

           127.0.0.1:8084/user/u1:

         可以看到,通过自定义注解的方式,就可以匹配多个无规则的方法。 

6.Spring AOP 的实现方式

(1)基于注解 @Aspect (参考上述)

(2)基于自定义注解(参考自定义注解 @annotation 部分的内容)

ps:本文到这里就结束了,谢谢观看;

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP 中,配置上下文可以通过以下步骤完成: 1. 在 Spring 配置文件中引入 aop 命名空间,如下所示: ```xml <beans xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> ``` 2. 定义切面类,其中包含切点和通知的定义。例如,创建一个名为 `LoggingAspect` 的切面类: ```java public class LoggingAspect { // 定义切点 @Pointcut("execution(* com.example.service.*.*(..))") private void serviceMethods() {} // 定义前置通知 @Before("serviceMethods()") public void beforeAdvice() { System.out.println("Before advice executed!"); } // 定义后置通知 @After("serviceMethods()") public void afterAdvice() { System.out.println("After advice executed!"); } } ``` 3. 在 Spring 配置文件中配置切面类的 bean: ```xml <bean id="loggingAspect" class="com.example.aspect.LoggingAspect" /> ``` 4. 配置 AOP 代理,将切面应用到目标对象上。可以使用 `<aop:config>` 元素来配置代理。例如: ```xml <aop:config> <aop:aspect ref="loggingAspect"> <aop:before method="beforeAdvice" pointcut-ref="serviceMethods" /> <aop:after method="afterAdvice" pointcut-ref="serviceMethods" /> </aop:aspect> </aop:config> ``` 在上述配置中,`ref` 属性指定了切面类的 bean ID,`method` 属性指定了通知方法的名称,`pointcut-ref` 属性指定了切点的引用。 通过以上配置,你可以在目标对象的方法执行前后执行相应的通知方法。注意,上述示例中的切点表达式是一个简单的示例,你需要根据实际情况进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值