当前位置: 首页 > news >正文

【JavaEE】(21)Spring AOP

一、什么是 Spring AOP

        AOP(AspectOrientedProgramming)即面向切面编程。切面可以理解为某一类特定问题用某一类特定方法解决,通俗点讲就是面向特定方法编程。因此,AOP 是一种编程思想,之前学习的 Spring 统一功能处理就是 Spring 实现的 AOP 思想,即 Spring AOP。当然不止于此,我们还需要学习利用 Spring 自定义地实现 AOP 开发,比如监控每个接口逻辑执行的耗时。这种思想不仅能实现统一处理,减少冗余代码;还能在不改变原有代码的基础上增加接口的功能

二、Spring AOP 开发快速入门

1、引入依赖

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

2、实现耗时监控

package com.edu.spring.apo.demo.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Slf4j
@Aspect // 标识该类为一个切面
@Component // 注册为 Spring Bean
public class TimeRecordAspect {// AOP 执行时机:Around 方法调用前后// 括号里:AOP 作用范围@Around("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pj) { // pj 是目标方法// 目标方法执行前逻辑long start = System.currentTimeMillis();try {return pj.proceed(); // 执行目标方法} catch (Throwable e) { // 执行 proceed() 会抛出异常throw new RuntimeException(e);} finally {// 目标方法执行后逻辑long cost = System.currentTimeMillis() - start;// 目标方法签名、耗时log.info("Method: {}, Cost: {} ms", pj.getSignature().getName(), cost);}}
}

三、Spring AOP 详解

1、核心概念

  • 切点:通知范围。如切点表达式 execution(* com.example.demo.controller.*.*(..))。
  • 连接点:通知对象。即目标方法。
  • 通知:通知内容。通知类型+重复逻辑。如 Around 和计时逻辑。
  • 切面:切点+连接点+通知。

2、通知类型

  • @Around 环绕通知:目标方法前后执行。
  • @Before 前置通知:目标方法执行。
  • @After 后置通知:目标方法执行,无论目标方法是否抛出异常都执行
  • @AfterReturning 返回后通知:目标方法执行,有异常不执行。
  • @AfterThrowing 异常后通知异常发生后执行。

测试代码:

package com.edu.spring.apo.demo.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 {@Around("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public Object doAround(ProceedingJoinPoint joinPoint) {log.info("Around 前...");Object result = null;try {result =  joinPoint.proceed();}catch (Throwable e) {log.error("执行目标方法抛出异常: " + e.getMessage());} finally {log.info("Around 后...");}return result;}@Before("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public void doBefore() {log.info("Before 前...");}@After("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public void doAfter() {log.info("After 后...");}@AfterReturning("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public void doAfterReturning() {log.info("AfterReturning 后...");}@AfterThrowing("execution(* com.edu.spring.apo.demo.controller.*.*(..))")public void doAfterThrowing() {log.info("AfterThrowing 后...");}
}

示例,执行目标方法无异常

示例,执行目标方法有异常

最常用的是 @Around,因为它可以包含以上所有类型通知的功能。

3、@PointCut 切点定义

        作用:提取公共切点表达式,减少冗余代码。使用时用 方法名(当前前面类)/全限定类名.方法名(其它切面类)引入。

4、@Order 切面优先级

        定义 3 个切面类,按 AspectDemo2、AspectDemo3、AspectDemo4 命名:执行顺序默认按切面类名字母排序。执行目标方法前,字母排名靠前的先执行;执行目标方法后,字母排名靠后的先执行。

        使用 @Order 指定切面类优先级数字小的优先级更高


5、切点表达式

(1)execution() 根据方法签名匹配

        语法:

访问修饰符、异常可省略 
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
  • * :匹配任意符号。
  • .. :匹配多个连续的任意符号。

        示例:匹配 com.example.demo.controller 包名下的所有类的所有方法。(类名、方法名、返回值类型、参数列表都任意。参数列表用 .. 是因为有 0~n 个参数,即连续任意符号)

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

(2)@annotation() 根据注解匹配

        execution() 指定的通知范围比较有规则,遇到通知范围是:一个类的部分方法、另一个类的部分方法,这种不规则的情况不适用了。

        使用流程:准备测试代码。

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {@RequestMapping("/t1")public String test1() {log.info("执行 t1 方法");return "t1";}@RequestMapping("/t2")public String test2() {log.info("执行 t2 方法");
//        Integer i = 10/0;return "t2";}
}
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/u1")public String test1() {log.info("执行 u1 方法");return "u1";}@RequestMapping("/u2")public String test2() {log.info("执行 u2 方法");
//        Integer i = 10/0;return "u2";}
}
  • 自定义注解

package com.edu.spring.apo.demo.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 {
}

@Target:作用范围是方法。

@Retention:注解生命周期是运行时。SOURCE 注解仅存于源码,如 lombok 提供的 @Data。CLASS 存于源码、字节码。RUNTIME 注解存于源码、字节码、运行时。

  • 使用 @annotation 定义切点表达式,作用于注解了 @MyAspect 的方法。

  • 在作用方法上添加自定义注解。这个注解不限于自定义注解,也可以是框架提供的注解。

6、Spring AOP 的实现方式(使用方式)

参考:

面试官:谈谈你对IOC和AOP的理解及AOP四种实现方式[通俗易懂]-腾讯云开发者社区-腾讯云https://cloud.tencent.com/developer/article/2032268

(1)配置 xml 的方法

  • 基于代理实现:最原始的方法。定义被代理类 (baby)、通知类(babyHelper)。xml 文件配置被代理、切点(sleep 方法)、通知对象,将通知与切点连接成切面,将切面切入到被代理对象
  • 使用 Spring AOP 接口实现:定义被代理类 (UserServiceImpl)、通知类(BeforeLog、AfterLog)。xml 文件注册被代理、通知对象,配置切点表达式连接通知为切面。Spring 自动管理对象,自动将切面切入注册的被代理对象,无需配置

(2)纯注解的方法

        五大注解注册对象。@Aspect 标识切面类,@Around 等标识通知类型,配上两种切点表达式 execution 或 @annotation。

四、Spring AOP 原理

        Spring AOP 是基于动态代理实现 AOP 的。因此需要学习代理模式。

1、代理模式

        又叫委托模式。有时希望被代理对象直接被访问,只能通过代理对象间接访问被代理对象。这样就能实现在不改变被代理对象的基础上通过新增代理对象来扩展被代理对象的功能

        根据代理类的创建时机分为:

  • 静态代理:程序运行,就通过编码存在。
  • 动态代理:程序运行,通过反射机制动态创建。

1.1、静态代理

        以租房为例,房东被代理者,中介是代理者。

        业务接口类:

package com.edu.spring.apo.demo.proxy;public interface HouseSubject {void rentHouse();
}

        业务实现类(被代理类):

package com.edu.spring.apo.demo.proxy;public class RealHouseSubject implements HouseSubject {@Overridepublic void rentHouse() {System.out.println("我是房东,正在出租房屋");}
}

        代理类(Proxy),在程序运行前就存在:

package com.edu.spring.apo.demo.proxy;public class HouseProxy implements HouseSubject{private RealHouseSubject realHouseSubject;public HouseProxy(RealHouseSubject realHouseSubject) {this.realHouseSubject = realHouseSubject;}@Overridepublic void rentHouse() {System.out.println("我是中介,开始代理");realHouseSubject.rentHouse();System.out.println("我是中介,结束代理");}
}

        测试:

package com.edu.spring.apo.demo.proxy;public class Main {public static void main(String[] args) {// 静态代理RealHouseSubject realHouseSubject = new RealHouseSubject();HouseProxy houseProxy = new HouseProxy(realHouseSubject);houseProxy.rentHouse();}
}

        静态代理有一个缺点,每增加/减少一个被代理类的重写方法,就需要增加/减少代理类的重写方法。每增加一个被代理类,就需要增加对应的代理类。总之,代理类会受被代理类影响

        比如,房东新增了售房业务,中介代理也需要跟着新增售房业务代理。

1.2、动态代理

        动态代理,在程序运行时才针对某一个被代理对象生成对应的代理对象。不需要硬编码代理类来重写每个方法去分别执行特定被代理对象的不同方法。而是通过反射动态获取被代理对象的类和方法,从而动态生成特定被代理对象对应的代理对象。

        在框架开发这种较低层开发中经常用到动态代理,我们学习动态代理的目的就是助于理解 Spring AOP 源码。

(1)JDK 动态代理

        JDK 动态代理的致命缺点只能代理实现了接口的类。但是实际开发中有很多类没有实现接口,而是直接实现的类。没有实现接口的类需要 CGLIB 动态代理

  • 定义接口、实现类。
  • 定义 JDK 动态代理类:实现 InvocationHandler 接口,重写 invoke 方法。

     

package com.edu.spring.apo.demo.proxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class JDKInvocationHandler implements InvocationHandler {private Object target;public JDKInvocationHandler(Object target) {this.target = target;}/*** @param proxy 代理实例* @param method 代理实例的所有接口方法* @param args 代理实例的所有接口方法的所有参数*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("我是中介,开始代理");// 通过反射调用代理对象的方法Object result = method.invoke(target, args);System.out.println("我是中介,结束代理");return result;}
}
  • 创建并使用代理对象:
        // JDK 动态代理RealHouseSubject realHouseSubject = new RealHouseSubject();JDKInvocationHandler jdkInvocationHandler = new JDKInvocationHandler(realHouseSubject);// 参数:被代理类对象类加载器;被代理类实现的接口;实现了 InvocationHandler 的对象HouseSubject houseProxy = (HouseSubject) Proxy.newProxyInstance(RealHouseSubject.class.getClassLoader(),new Class[]{HouseSubject.class},jdkInvocationHandler);houseProxy.rentHouse();
(2)CGLIB 动态代理

        可以动态代理没有实现接口的类

  • 引入依赖:因为 CGLIB 是第三方工具,所以需要配置依赖。但是 Spring Boot 也集成了 CGLIB 的接口,所以可以配置依赖,但是需要用 Spring Boot 集成的 CGLIB 包。
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>

  • 定义实现类。
  • 实现 MethodInterceptor 接口,重写 intercept 方法。
package com.edu.spring.apo.demo.proxy;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CGLIBInterceptor implements MethodInterceptor {private Object target;public CGLIBInterceptor(Object target) {this.target = target;}/*** @param o 被代理对象* @param method 被代理对象的目标方法* @param objects 方法的参数数组* @param methodProxy 用于调用代理对象的方法*/@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {System.out.println("我是中介,开始代理");// methodProxy.invoke(target, objects) 也行Object result = method.invoke(target, objects);System.out.println("我是中介,结束代理");return result;}
}
  • 创建并使用代理对象:
        // CGLIB 动态代理RealHouseSubject realHouseSubject = new RealHouseSubject();CGLIBInterceptor interceptor = new CGLIBInterceptor(realHouseSubject);// 参数:被代理类类型;实现 MethodInterceptor 的自定义方法拦截器HouseSubject houseProxy = (HouseSubject) Enhancer.create(RealHouseSubject.class,interceptor);houseProxy.rentHouse();

        运行报错:

        原因:Java 9 开始默认核心代码的包是封闭的,不允许外部代码,如 CGLIB 通过反射访问。使用 CGLIB 要添加 JVM 启动参数:

--add-opens java.base/java.lang=ALL-UNNAMED

2、Spring AOP 源码(了解)

(1)Spring AOP 的默认代理类型

        源码中的代理工厂有一个属性 proxyTargetClass,对于 Spring Boot 2.x 及以上,默认true;而 Spring 默认是 false

proxyTargetClass目标对象代理方式
false实现了接口的类JDK 代理
false未实现接口的类CGLIB 代理
true实现了接口的类CGLIB 代理
true未实现接口的类CGLIB 代理

        Spring Boot3 默认值 true,测试被代理对象为普通类、实现接口的类命名,都为 CGLIB 代理:

@SpringBootApplication
public class SpringApoDemoApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringApoDemoApplication.class, args);// 默认 true// 实现了接口,CGLIB 代理RealHouseSubject realHouseSubject = (RealHouseSubject) context.getBean("realHouseSubject");System.out.println(realHouseSubject.getClass().toString());// 未实现接口,CGLIB 代理TestController testController = context.getBean(TestController.class);System.out.println(testController.getClass().toString());}}

        修改配置项参数为 false:

spring:aop:proxy-target-class: false

        报错:因为实现了接口的类,为 JDK 代理,它只能代理接口,所以强转的类型应该是接口。

        修改类型:

(2)源码

        Spring AOP 的实现主要在 AnnotationAwareAspectJAutoProxyCreator 中,生成代理对象的逻辑主要在父类 AbstractAutoProxyCreator 中的 createProxy 方法实现:

        getProxy 有两种实现:

http://www.dtcms.com/a/362762.html

相关文章:

  • 解密GTH时钟架构:一网打尽收发器时钟之谜
  • 火语言 RPA 界面应用生成:低代码逻辑下的功能设计与场景适配
  • PowerPoint和WPS演示如何循环放映PPT
  • 想找Gamma的平替?这几款AI PPT工具值得试试
  • 从技术架构到经济价值:低代码在企业开发中的成本节约潜力
  • LeetCode 925.长按键入
  • 哈希表-面试题01.02.判定是否互为字符重排-力扣(LeetCode)
  • 趣味学RUST基础篇(HashMap)
  • 二叉树的非递归遍历 | 秋招面试必备
  • Spring Bean
  • LLM面试50问:NLP/RAG/部署/对齐/安全/多模态全覆盖
  • R语言根据经纬度获得对应样本的省份
  • WPF依赖属性和依赖属性的包装器:
  • iOS混淆工具实战 视频流媒体类 App 的版权与播放安全保护
  • 安卓学习 之 gradle下载失败的解决方法
  • Elasticsearch面试精讲 Day 5:倒排索引原理与实现
  • 跨越产业技术障碍、创新制造模式的智慧工业开源了
  • 【开题答辩全过程】以宠物生活社区为例,包含答辩的问题和答案
  • 扩散模型驱动的智能设计与制造:下一场工业革命?
  • 最新!阿里财报电话会蒋凡与吴泳铭透露重要信息:淘宝闪购成绩斐然;零售与AI双轮驱动;阿里云推出“Agent Bay”新产品···
  • 物联网为何离不开天硕工业级SSD固态硬盘?
  • maven 常用指令
  • Corona渲染噪点终结指南:3ds Max高效去噪全攻略
  • 【3D 入门-3】常见 3D 格式对比,.glb / .obj / .stl / .ply
  • 通信中FDD和TDD的区别
  • 【SpringBootWeb开发】《一篇带你入门Web后端开发》
  • 242. 有效的字母异位词| 349. 两个数组的交集
  • 框架-SpringMVC-1
  • 手写Muduo网络库核心代码1-- noncopyable、Timestamp、InetAddress、Channel 最详细讲解
  • hive udf 执行一次调用多次问题