【Java EE】Spring AOP :面向切面编程

文章目录

IoC与AOP是 Spring 两大核心。AOP是编程思想,Spring AOP是这套思想的落地实现,能够在 不修改原有业务代码 的前提下对方法做功能增强,非常适合做日志统计、耗时监控、权限校验、异常处理等通用功能。

一、什么是AOP

AOP(Aspect Oriented Programming)面向切面编程,对一类通用问题做集中处理。AOP思想:无侵入增强;不改动业务源代码,就给方法追加功能。

日常开发中AOP思想的体现:

  1. 拦截器:做登录校验,针对请求URL拦截
  2. @ControllerAdvice:全局异常处理、数据预处理
  3. Spring AOP:粒度更细,可以按照包、类、方法名、方法注解、参数拦截方法。

拦截器与Spring AOP对比:拦截器作用于URL请求;Spring AOP作用于Java方法,颗粒更细

举个例子:统计项目各个业务方法的执行耗时。

如果不用AOP,要在每一个方法开头、结尾手写获取时间戳,业务代码到处充斥重复统计代码,维护麻烦。

AOP可以抽取通用逻辑,统一增强目标方法。

AOP只是一种思想,由Spring AOP、AspectJ、CGLIB等方法实现

二、Spring AOP快速入门

1、引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
	<!-- Springboot4将aop改名未aspectj,springboot3还是spring-boot-starter-aop-->
    <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>

2、编写切面类

需求:统计controller包下所有方法的执行耗时

java 复制代码
@Aspect  
@Slf4j  
@Component
public class TimeAspect {
    //环绕通知,切点表达式匹配controller下所有类所有任意参数方法
	 @Around("execution( * com.daisy.springbookdemo.controller.*.*(..))")  
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录开始时间
        long startTime = System.currentTimeMillis();
        //执行原始业务方法
        Object proceed = pjp.proceed();
        //记录结束时间
        long endTime = System.currentTimeMillis();
        //打印方法签名与耗时
        log.info("{} 执行耗时:{} ms",pjp.getSignature(), endTime-startTime);
        return proceed;
    }
}

运行项目,访问controller接口,控制台就会输出每个方法的执行耗时。

AOP带来的优势:

  1. 代码无侵入:完全不用改动业务方法
  2. 消除重复样板代码
  3. 开发效率高,后续维护只需要修改切面类

三、SpringAOP详解

3.1 四个核心概念

  • 切点 Pointcut:筛选规则

    通过 execution / @annotation 写表达式,筛选一批符合条件的方法 ,用来匹配哪些方法需要增强

    execution(* com.xxx.controller.*.*(..)) 就是筛选 controller 包下所有方法。

  • 连接点 JoinPoint:可以被拦截的点,满足切点表达式的那些方法

    SpringAOP 中只有方法执行是连接点,例如UserController.query()这个方法。

  • 通知 Advice:要执行的增强逻辑(一段 Java 方法)

    @Before/@Around/@After修饰的方法,就是通知方法。

  • 切面 Aspect = 切点 + 通知

    切面类里面:定义筛选哪些方法(切点) + 对这些方法做什么增强(通知)。

以上面的执行耗时为例子:

3.2 五大通知类型

SpringAOP一共5种通知,控制增强逻辑在目标方法什么时机执行。

注解 通知类型 执行时机
@Around 环绕通知 目标方法前后都执行;可以控制原始方法是否执行;最强大
@Before 前置通知 目标方法执行之前执行
@After 后置通知 目标方法执行之后执行,无论是否抛异常一定会执行
@AfterReturning 返回通知 目标方法正常返回后执行;出现异常不会执行
@AfterThrowing 异常通知 只有目标方法抛出异常的时候才执行

代码示例

java 复制代码
@Aspect  
@Component  
@Slf4j  
public class AspectDemo1 {  
    //环绕通知,切点表达式匹配controller下所有类所有任意参数方法  
    @Around("execution( * com.daisy.aop.controller.*.*(..))")  
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {  
        //记录开始时间  
        long startTime = System.currentTimeMillis();  
        log.info("目标方法执行前");  
        //执行原始业务方法  
        Object proceed = pjp.proceed();  
        //记录结束时间  
        long endTime = System.currentTimeMillis();  
        log.info("目标方法执行后");  
        log.info("{} 执行耗时:{} ms", pjp.getSignature(), endTime - startTime);  
        return proceed;  
    }  
  
    @Before("execution( * com.daisy.aop.controller.*.*(..))")  
    public void doBefore() {  
        log.info("doBefore");  
    }  
  
    @After("execution( * com.daisy.aop.controller.*.*(..))")  
    public void doAfter() {  
        log.info("doAfter");  
    }  
  
    @AfterReturning("execution( * com.daisy.aop.controller.*.*(..))")  
    public void doAfterReturning() {  
        log.info("doAfterReturning");  
    }  
  
    @AfterThrowing("execution( * com.daisy.aop.controller.*.*(..))")  
    public void doAfterThrowing() {  
        log.info("doAfterThrowing");  
    }  
}

执行顺序

  1. 目标方法正常执行无异常
  2. 目标方法抛出异常

    Around的proceed抛出异常,Around后置代码不会执行;AfterReturning不会执行

注意:

  1. @Around环绕通知,必须调用pjp.proceed(),否则原始业务方法不会执行;
  2. 环绕通知返回值必须写Object,接收原始方法返回结果;
  3. 除了@Around,其他通知不用手动调用原始方法。
  4. @Around可以替换其余四个通知类型:
    • 目标方法执行前:相当于@Before
    • try内部,目标方法执行后:相当于@AfterReturning,没有异常抛出才执行
    • catch内部:相当于@AfterThrowing,有异常抛出时执行
    • finally内部:相当于@After,无论是否抛出异常都会执行
    • finally之后的部分:相当于@AfterReturning,没有异常抛出才执行
java 复制代码
@Around("execution( * com.daisy.aop.controller.*.*(..))")  
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {  
    log.info("目标方法执行前");  
    Object result = null;  
    
    //执行原始业务方法  
    try{  
        result = pjp.proceed();   
    }catch (Throwable e){  
        log.error("do around throwing");  
    }finally{
	    log.info("收尾工作")
    }
    
    log.info("目标方法执行后");  
    
    return result;  
}

@Pointcut:复用切点表达式

把重复的切点抽取到一个无实现的方法上。

java 复制代码
@Pointcut("execution( * com.daisy.aop.controller.*.*(..))")  
public void pt(){}  
  
@Around("pt()")  
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
	//...
}

pt()方法的访问修饰符决定它的作用域,如果设置为private就只能在当前类中使用;

如果设置为public,在AspectDemo2中使用AspectDemo1的pt()方法:

@Around("com.daisy.aop.aspect.AspectDemo1.pt()")

3.3 多切面执行顺序 @Order

当多个切面同时拦截同一个目标方法,就会涉及执行优先级。

默认情况按照切面类的名称排序,但是实际开发中不可能根据顺序改动类名,因此引入了@Order(数字)手动控制优先级

  • @Before前置通知:数字越小,越先执行
  • @After后置通知:数字越大,越后执行

切面就像一层层同心圆,Order值越小越靠外层,先进后出。

3.4 切点表达式详解

两种最常用写法:execution()@annotation()

1、execution():按方法签名匹配

语法:

复制代码
execution(<访问修饰符> <返回类型> <包.类.方法(参数)>)

访问修饰符、异常可以省略。

通配符:

  1. *:匹配单个任意元素(返回值、包一层、类名、方法名、单个参数)
  2. ..:匹配多层;包中代表当前包以及所有子包;参数位置代表任意数量任意类型参数

示例:

java 复制代码
//controller包下所有类所有任意参数方法
execution(* com.example.demo.controller.*.*(..))

//demo包以及所有子孙包全部方法
execution(* com.example.demo..*(..))

//TestController类下全部无参方法
execution(* com.example.demo.controller.TestController.*())

2、@annotation():自定义注解匹配切点

适合场景:方法分散在不同包不同类,不方便通过包路径匹配,给需要增强的方法打上自定义注解,切面拦截该注解。

实现步骤:

  1. 自定义运行时注解
java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD) //只能标注方法
@Retention(RetentionPolicy.RUNTIME) //运行时保留,可以反射读取
public @interface MyAspect {
}
  1. 切面:使用@annotation切点
java 复制代码
@Slf4j
@Aspect
@Component
public class MyAspectDemo {
    //拦截标记了@MyAspect注解的所有方法
    @Before("@annotation(com.example.demo.aspect.MyAspect)")
    public void before(){
        log.info("自定义注解切面执行");
    }
}
  1. 在需要增强的业务方法上加注解
java 复制代码
@MyAspect
@RequestMapping("/t1")
public String t1(){
    return "t1";
}

不一定要使用自定义注解,也可以拦截spring的注解,例如@annotation(org.springframework.web.bind.annotation.RequestMapping)可以拦截所有加了RequestMapping注解的方法

四、SpringAOP底层原理

Spring AOP底层基于动态代理实现,分为 JDK动态代理、CGLIB动态代理。

4.1 代理模式

代理模式也称为委托模式,是指为其他对象提供一种代理,以控制这个对象的访问。它的作用就是提供一个代理类,让我们调用目标方法的时候不直接调用目标方法,而是通过代理类间接调用

某些情况下,一个对象不适合或者不能直接引用另一个对象,代理对象就可以在客户端与目标对象之间起到中介的作用

代理模式核心:不修改目标对象,通过代理对象间接调用目标方法,在代理中增加增强逻辑。

使用代理前:

使用代理后:

类比:房屋中介、经销商、助理/秘书都可以认为是代理对象

代理模式中的主要角色

  1. Subject:业务接口类,可以是抽象类或者接口,非必要存在
  2. RealSubject:业务实现类,具体的业务执行,也就是被代理对象
  3. Proxy:代理类,RealSubject的代理

4.2 静态代理

编译期就已经生成代理class文件。

缺点:每一个目标类都要写一套代理类,接口改动代理类也要同步修改,代码冗余。

4.3 动态代理:运行时生成代理class

1. JDK动态代理

前提:目标类必须实现接口,基于接口生成代理类

  1. 定义业务接口,编写目标实现类(也就是被代理对象,实现该接口)。
  2. 自定义类实现InvocationHandler接口,重写invoke()方法;在invoke中编写增强逻辑 ,通过method.invoke(target,args)反射调用目标对象原始方法。
  3. 调用Proxy.newProxyInstance(类加载器, 目标实现的接口数组, InvocationHandler实例)运行时动态生成实现了目标接口的代理 Class 字节码,并创建代理对象
  4. 调用代理对象的方法,方法请求会转发到InvocationHandlerinvoke方法;先执行自定义增强逻辑,再执行目标方法,之后执行后置增强,返回结果。
  • 核心API:InvocationHandlerProxy.newProxyInstance()
  • 缺点:没有实现接口的普通类,JDK代理无法使用。

2. CGLIB动态代理

前提:目标类不需要实现接口,通过继承目标类生成子类作为代理类

  1. 编写普通目标类,不需要实现接口。
  2. 自定义类实现MethodInterceptor接口,重写intercept()方法;在 intercept 中编写增强逻辑,通过methodProxy.invoke(target,objects)调用原始目标方法。
  3. 使用Enhancer工具,设置被代理的目标类 Class、注册MethodInterceptor回调;运行时动态生成目标类的子类字节码,创建代理对象。
  4. 调用代理对象方法,请求进入intercept;执行自定义增强逻辑,调用父类(原目标)方法,执行后置增强,返回结果。
  • 核心API:MethodInterceptorEnhancer
  • 注意:被final修饰的类无法被继承,CGLIB不能代理final类。

区别

  1. JDK 代理:依赖接口;InvocationHandler#invokeProxy生成代理。
  2. CGLIB 代理:继承目标类,既可以代理类也可以代理接口;MethodInterceptor#interceptEnhancer生成代理。

4.4 Spring AOP怎么选择代理方式

配置属性 proxyTargetClass

proxyTargetClass 目标对象类型 代理方式
false 实现接口 JDK动态代理
false 普通类无接口 CGLIB
true 无论是否接口 CGLIB

Spring AOP 选择规则:目标实现接口默认 JDK;无接口则 CGLIB;设置proxyTargetClass=true强制使用 CGLIB。

大多数情况SpringBoot和Spring保持一致,但是这里存在差异:

  • SpringFramework中proxyTargetClass默认值为false,也就是说根据是否有实现接口来选择代理方式
  • SpringBoot2.x与上面相同
  • SpringBoot3.x以及之后proxyTargetClass默认值为true,也就是说默认使用CGLIB代理

可以在配置文件修改:

properties 复制代码
# 使用jdk动态代理
spring.aop.proxy-target-class=false
相关推荐
软件开发JR1 小时前
基于Web的足球青训俱乐部管理后台系统的设计与开发
java·前端·spring boot·毕业设计
凤山老林2 小时前
基于 Spring Batch 的海量数据迁移与批处理架构:分片、容错与断点续跑
java·spring boot·spring·架构·spring batch
Javatutouhouduan2 小时前
Java初学者如何高效学习JVM?
java·jvm·java虚拟机·java面试·后端开发·java程序员·java八股文
ZJU_统一阿萨姆2 小时前
【算子开发】全局内存访问与合并访存
java·服务器·网络·人工智能·语言模型
7177772 小时前
不止工具集成:基于 Gitee 软件工厂构建 DevSecOps 研发治理底座
java·服务器·gitee
gis开发之家3 小时前
Spring Boot 4 深度解析——参数接收大全:@RequestParam、@PathVariable、@RequestBody
java·spring boot·后端·spring
(轻舟已过万重山)3 小时前
D1 · 融合蓝图:Spring AI + 虚拟线程 + 服务网格——现代后端统一底座
java·人工智能·spring
SamDeepThinking3 小时前
警惕那些很长时间没有编写任何代码、却在设计系统的人
java·后端·架构
莫得感情 o3 小时前
并发 13 · 异步编排
java·并发