【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. 调用代理对象的方法,方法请求会转发到InvocationHandler的invoke方法;先执行自定义增强逻辑,再执行目标方法,之后执行后置增强,返回结果。
  • 核心API:InvocationHandler、Proxy.newProxyInstance()
  • 缺点:没有实现接口的普通类,JDK代理无法使用。

2. CGLIB动态代理

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

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

区别

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

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
相关推荐
xcl09251 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐3 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长3 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn3 小时前
Spring 注解使用详解
java·spring
一条小小yu3 小时前
Spring IoC的理解
java·后端·spring
落魄实习生4 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈4 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白5 小时前
核心功能(Service接口)
java·mybatis
乌暮6 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习