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

【Java EE】Spring AOP

1. 初识 AOP

        AOP 全称 Aspect Oriented Programming,意为面向切面编程。切面指一类特定的,可以被统一处理的问题,因此 AOP 可以理解为面向特定问题编程。Spring 提供的拦截器就是对 AOP 思想的一种实现。除了解决登录校验的问题,我们还可能遇到许多其他的,需要被统一处理的问题,这些问题就需要使用 Spring 对 AOP 的支持来实现。

        因此,简单来说 AOP 就是一种将一类问题集中处理的思想。

        Spring AOP 是 AOP 思想的具体实现,或者说 Spring 对 AOP 的支持。除此之外,AspectJ、CGLIB 等都是 AOP 思想的实现。

        假设现在有一个需求,记录项目中所有业务方法的执行时间。我们需要在每一个业务方法的执行前后分别记录时间戳,这是很大的工作量,并且它们是重复的劳动。而如果使用 AOP 的思想,将这些工作提取出来统一处理,就能减少大部分重复工作。并且 AOP 可以在不对原有方法做改动的基础上,只进行功能增强,因此这也是一种低耦合的开发方式。

2. Spring AOP 的使用

        1. 在 pom 文件中添加相应依赖。

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

        2. 确定待增强的方法。 目标:分别统计下面三个方法的执行时间。

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@RequestMapping("/t1")public Integer t1() {log.info("执行 t1");return 1;}@RequestMapping("/t2")public Boolean t2() {log.info("执行 t2");return true;}@RequestMapping("/t3")public String t3() {log.info("执行 t3");return "hello";}
}

        3. 写切面,描述当前遇到的统一问题,增强原方法。

@Aspect // 标识这是一个切面类
@Component // 需要将该类交给 Spring 管理
@Slf4j
public class AspectDemo1 {// Around 环绕通知,表示通知可以在方法执行前,也可以在方法执行后@Around("execution(* com.boilermaker.springaoplearning.controller.*.*(..))")public Object recordTime(ProceedingJoinPoint pjp) {// 前置通知(打印时间戳)log.info("目标方法执行前...");long begin = System.currentTimeMillis();// 使用 Object 接收原始方法Object result;try {result = pjp.proceed(); // 执行原始方法} catch (Throwable e) {log.info("目标方法抛出异常..."); // 异常后通知throw new RuntimeException(e);}// 后置通知(打印时间戳,计算耗时)log.info("目标方法执行后...");long end = System.currentTimeMillis();log.info(pjp.getSignature() + " 执行耗时:" + (end - begin) + " ms");return result;}
}

3. 代码详解

3.1 切点 Pointcut

        告诉程序对哪些方法来进行功能增强。下面的代码就是切点表达式。

"execution(* com.boilermaker.springaoplearning.controller.*.*(..))"

        execution( 访问修饰符 返回类型 包 - 类 - 方法 ( 方法参数 ) 异常 )

        其中访问修饰符和异常是选填。 

        通配符表达:* 可以表示任意一个元素,如一层包、一个类、一个方法、一个返回类型、一个参数。.. 表示任意个连续的元素,如任意层包、任意个参数。

        例:

        我们也可以基于 @annotation 注解写针对多个无规则方法的切点表达式。比如 Controller1 中的 t1 方法和 Controller2 中的 t2 方法,这两个方法之间没有什么直接关联。此时,我们需要自定义注解,再使用 @annotation 表达切点,再在待增强方法上添加注解。

        1. 创建注解类,声明注解。

@Target(ElementType.METHOD) // 描述该注解可以被添加在什么地方
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期
public @interface TimeMonitor {}

         2. 编写切面类,和刚刚的代码基本一致,只是需要使用 @annotation 注解定义切点。下面的切点表达式意为只在加 TimeMonitor 注解的方法上做增强。

@Around("@annotation(com.boilermaker.springaoplearning.aspect.TimeMonitor)")

        3.  为目标方法添加 @TimeMonitor 注解。在这里我为 t1 加该注解。

        分别请求 t1 和 t2,发现 t1 被增强,t2 无效果,符合预期。

        对于相同的切点表达式,我们不需要写很多遍,可以使用 @Pointcut 注解将公共的切点表达式提取出来。

    @Pointcut("execution(* com.boilermaker.springaoplearning.controller.*.*(..))")public void controllerPointcut() {}...// 后续使用时直接引用即可   @Around("controllerPointcut()")

 3.2 切面 Aspect

        使用切面来描述整个 AOP 程序,包括它针对于哪些方法,在什么时候执行什么样的操作。

        切面所在的类,称为切面类,需要添加 @Aspect 注解。

        当我们对同一个目标方法,应用多个切面类时,应使用 @Order 注解控制多个切面的执行顺序。其优先级遵循下图所示规则。

        

3.3 通知 Advice

        通知就是我们具体要增强的那部分功能,也就是提取出的统一问题。

        除了 @Around 环绕通知外,Spring 也单独提供了 @Before 前置通知、@After 后置通知、@AfterReturning 返回后通知和 @AfterThrowing 异常后通知。

        其中 @After 和 @AfterReturning 的区别仅仅在于目标方法是否成功执行。如果目标方法执行成功,它们都可以正常增强,如果目标方法执行失败,@AfterReturning 将会失效,@After 依然可以正常增强。

        @Around 更加自由,因为它在代码中调用 pjp.proceed() 来自由控制目标方法执行时间。如果目标方法执行失败,那么写在 pjp.proceed() 后的逻辑不会执行。所以 @Around 和 @After 这两个通知类型就可以应对全部情况了。

3.4 连接点 Join Point

        连接点是满足切点表达式的元素,相当于切点的真子集。

        比如在下面的切点表达式中,controller 包下全部类的全部方法都是连接点。

"execution(* com.boilermaker.springaoplearning.controller.*.*(..))"

4. Spring AOP 原理

4.1 代理模式

        代理模式下,代理对象与真实对象实现相同的接口,客户端无需直接操作真实对象,而是通过代理对象间接访问,从而在访问过程中附加额外功能(如日志记录、权限校验、延迟加载等)或限制访问。代理模式的本质是功能增强。

        代理模式中的角色:

        Subject 业务接口:定义代理和真实对象的共同接口,客户端通过该接口访问对象。

        RealSubject 业务具体实现类:实现接口的具体业务逻辑,是代理的目标对象。

        Proxy 代理类:实现抽象接口,持有目标对象的引用,在调用目标对象前后添加额外操作。

        代理模式又分为静态代理和动态代理。我们很早就知道,Java 程序的运行需要经历,.java 文件的编写,编译器将其编译为 .class 文件,JVM 读取 .class 文件并执行这几个过程。静态代理就是指,代理类是由开发者自行编写,其对应的 .class 文件在运行前存在。相对的,动态代理的代理类由 JVM 来实现,在程序运行时根据需要动态生成。

 4.1.1 静态代理

-----> Subject 抽象接口
public interface UserService {String queryUser(String id);
}-----> RealSubject 实现用户服务的具体逻辑
public class RealUserService implements UserService {@Overridepublic String queryUser(String id) {return "用户信息:id=" + id; // 实际业务逻辑}
}-----> Proxy 代理持有目标对象引用,添加日志功能
public class UserServiceProxy implements UserService {private UserService realUserService; // 持有真实对象public UserServiceProxy(UserService realUserService) {this.realUserService = realUserService;}@Overridepublic String queryUser(String id) {// 调用前:添加日志System.out.println("日志:开始查询用户,id=" + id);// 调用真实对象的方法String result = realUserService.queryUser(id);// 调用后:添加日志System.out.println("日志:查询用户结束,结果=" + result);return result;}
}
客户端通过代理间接访问目标对象
public class Client {public static void main(String[] args) {UserService realService = new RealUserService();UserService proxy = new UserServiceProxy(realService); // 创建代理proxy.queryUser("1001"); // 客户端调用代理}
}
输出:
日志:开始查询用户,id=1001  
用户信息:id=1001  
日志:查询用户结束,结果=用户信息:id=1001  

        静态代理不够灵活。假设 Subject 中增加了许多接口,那么 Proxy 也需要相应地进行增加,但是实际上我们对这些方法的增强功能都是一样的,这就是在做重复的工作。我们使用代理就是为了避免重复,但是静态代理实际上只是将代码换了个地方,并没有减少任何工作量。因此静态代理几乎用不上,它只是帮我们更好地理解代理思想。

4.1.2 动态代理

        对于动态代理来说,创建代理对象的工作被推迟到程序运行时,由 JVM 根据我们的需要来自行实现。

        我们知道,JVM 执行字节码前有一个关键步骤,就是 JVM 通过类加载,将 .class 文件加载到内存,生成 java.lang.Class 对象。通过 Class 对象可以获取类的全部信息,这是 Java 反射机制的基础。Java 的反射 API 本质就是对 Class 中信息的读取和操作,发生在运行时,因此与反射有关的操作我们往往加以动态二字。

        动态代理也不例外,它的核心就是反射机制。动态代理在运行时生成的代理类,本质就是动态生成的字节码。

        具体原理如下:

        先给出业务接口和其具体实现类。

// -----> Subject <-----
public interface UserService {void queryUser(String id);void deleteUser(String id);
}// -----> RealSubject <-----
public class RealUserService implements UserService {@Overridepublic void queryUser(String id) {System.out.println("查询中, id = " + id);}@Overridepublic void deleteUser(String id) {System.out.println("删除中, id = " + id);}
}

        在拦截器中定义增强逻辑,JDK 动态代理规定拦截器需实现 InvocationHandler 接口。

public class UserInvocationHandler implements InvocationHandler {private Object target;public UserInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置通知: ...");Object ret = method.invoke(target, args);System.out.println("后置通知: ...");return ret;}
}

        现在的需求是增强 queryUser 和 deleteUser 方法。

        使用动态代理,第一步,我们需要一个目标方法所在类的实例。它有两个作用,首先 Proxy 必须使用它的类加载器,并且它将作为参数传入拦截器。

UserService realUserService = new RealUserService();

        接下来,调用 Proxy.newProxyInstance() 方法来创建代理。

        UserService proxy = (UserService) Proxy.newProxyInstance(// 类加载器realUserService.getClass().getClassLoader(),// 被代理类实现的接口new Class[]{UserService.class},// 指定拦截器new UserInvocationHandler(realUserService));

        代码的核心是方法的第二个参数,目标方法所在类实现的接口。这个参数告知 Proxy,需要生成一个实现 UserService 接口的代理类。它将在运行时动态生成实现该接口全部方法的字节码。实现逻辑类似下面的代码。

// proxy 伪代码
public class proxy extends Proxy implements UserService {@Overridepublic void queryUser(String id) {// Proxy 首先通过反射将方法封装在 Method 中Method method = UserService.class.getMethod("queryUser", String.class);// 调用传入的拦截器,进行增强操作userInvocationHandler.invoke(this, method, new Object[]{id});}@Overridepublic void updateUser(String id) {Method method = UserService.class.getMethod("updateUser", String.class);userInvocationHandler.invoke(this, method, new Object[]{id});}
}

        对于拦截器中的 invoke 方法。第一个参数是代理对象本身,这个参数一般不会用到。第二个参数是 proxy 封装的方法对象,它用于 method.invoke(realUserService, args),即反射调用目标对象的真实方法。第三个参数是当前方法调用的参数列表,比如调用 queryUser("1010") 时,args 就是 new Object [] {"1010"}。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}

        拦截器中的 invoke 方法由自动生成的 proxy 调用,invoke 方法中依然存在一个 invoke 方法,这个 invoke 方法是连接代理与目标对象的关键,因为它是在通过反射调用 RealSubject 中的方法。 

Object ret = method.invoke(target, args);

        完整客户端代码如下。后续通过 proxy 调用方法,实际上调用的是 proxy 重写的方法。

public class Client {public static void main(String[] args) {UserService realUserService = new RealUserService();UserService proxy = (UserService) Proxy.newProxyInstance(realUserService.getClass().getClassLoader(),new Class[]{UserService.class},new UserInvocationHandler(realUserService));proxy.queryUser("1010");proxy.deleteUser("1010");}
}
http://www.dtcms.com/a/313139.html

相关文章:

  • Gumbel-Softmax函数
  • AI+预测3D新模型百十个定位预测+胆码预测+去和尾2025年8月3日第155弹
  • 数据与信息的边界:非法获取计算机信息系统数据罪的司法困境与出路
  • 【十九、Javaweb-day19-Linux概述】
  • python---可变类型、不可变类型
  • Pytorch 报错-probability tensor contains either ‘inf‘, ‘nan‘ or element < 0 解决方案
  • Arrays.asList() add方法报错java.lang.UnsupportedOperationException
  • 8月3日星期日今日早报简报微语报早读
  • 多线程(四) ~ wait,join,sleep及单例与工厂模式
  • 图像识别区分指定物品与其他物体
  • 【华为机试】815. 公交路线
  • NumPy库学习(三):numpy在人工智能数据处理的具体应用及方法
  • 机器学习sklearn:支持向量机svm
  • Vue3 其它Composition API
  • Linux网络编程 --- 多路转接select
  • 推送本地项目到Gitee远程仓库
  • Selenium Web 自动化
  • 优选算法 力扣 202.快乐数 快慢双指针 解决带环问题 C++解题思路 每日一题
  • ThinkPHP5x,struts2等框架靶场复现
  • Coin Combinations II(Dynamic Programming)
  • LLM - AI大模型应用集成协议三件套 MCP、A2A与AG-UI
  • 用 Eland 在 Elasticsearch Serverless 部署 Learning-to-Rank 排序模型
  • 数据,正在成为AI大模型最后的护城河
  • leetcode 2106. 摘水果 困难
  • Rust 同步方式访问 REST API 的完整指南
  • 道格拉斯-普克算法 - 把一堆复杂的线条变得简单,同时尽量保持原来的样子
  • python---赋值、浅拷贝、深拷贝
  • 【C 学习】03-你的第一个C程序
  • 上位机知识篇---脚本文件
  • Linux环境下使用Docker搭建多服务环境