《手写Spring渐进式源码实践》实践笔记(第十二章 aop融入bean生命周期)

提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 第十二章 将AOP融入Bean生命周期
    • 背景
    • 目标
    • 设计
    • 实现
      • 代码结构
      • 类图
      • 实现步骤
    • 测试
      • 事先准备
      • 自定义拦截方法
      • Spring.xml 配置AOP
      • 测试用例
      • 测试结果:
    • 总结


第十二章 将AOP融入Bean生命周期

背景

  1. 在上一章节我们基于 Proxy.newProxyInstance 代理操作中处理方法匹配和方法拦截,对匹配的对象进行自定义的处理操作。并把这样的技术核心内容拆解到 Spring 中,用于实现 AOP 部分,通过拆分后基本可以明确各个类的职责,包括你的代理目标对象属性、拦截器属性、方法匹配属性,以及两种不同的代理操作 JDK 和 CGlib 的方式。

  2. 基于目前实现的一个 AOP 核心功能,我们可以通过单元测试的方式进行验证切面功能对方法进行拦截。但如果这是一个面向用户使用的功能,就不太可能让用户这么复杂且没有与 Spring 结合的方式单独使用 AOP,虽然可以满足需求,但使用上还是过于分散。

目标

在本章节完成 AOP 核心功能接入Bean生命周期,实现与 Spring 框架的整合,最终能通过 Spring 配置的方式完成切面的操作。

设计

  • 其实在有了AOP的核心功能实现后,把这部分功能服务融入到 Spring 其实也不难,只不过要解决几个问题,包括:怎么借着 BeanPostProcessor 把动态代理融入到 Bean 的生命周期中,以及如何组装各项切点、拦截、前置的功能适配对应的代理器。整体设计结构如下图:

image-20241024160619361

  • 对象创建过程中,为了能把xml配置的代理对象、切面相关类对象实例化,这里需要用到BeanPostProcessor提供的方法,可以分别作用于Bean对象执行初始化前后修改Bean对象扩展信息。这里需要基于 BeanPostProcessor 实现新的接口和实现类,这样才能定向获取对应的类信息。

  • 创建的代理对象不是普通对象,因此需要前置于其他对象的创建。对应在AbstractAutowireCapableBeanFactory#createBean优先完成Bean对象的判断,如果需要代理,则直接返回代理对象。在Spring的源码中会有 createBean 和 doCreateBean 的方法拆分

  • 需要解决方法拦截器的具体功能,提供一些 BeforeAdvice、AfterAdvice 的实现,让用户可以更简化的使用切面功能。

  • 需要包装切面表达式以及拦截方法的整合,以及提供不同类型的代理方式的代理工厂,来包装我们的切面服务。

实现

代码结构

image-20241024171726100

源码实现:https://github.com/swg209/spring-study/tree/main/step12-aop-spring-bean

类图

image-20241025140745626

  1. 整个类关系图中可以看到,在以 BeanPostProcessor 接口实现继承的 InstantiationAwareBeanPostProcessor 接口后,做了一个自动代理创建的类 DefaultAdvisorAutoProxyCreator,这个类的就是用于处理整个 AOP 代理融入到 Bean 生命周期中的核心类。
  2. DefaultAdvisorAutoProxyCreator 会依赖于拦截器MethodBeforeAdviceInterceptor代理工厂ProxyFacotry和提供切面、拦截方法和表达式,包装了Pointcut与Advisor的服务 AspectJExpressionPointcutAdvisor
  3. Spring 的 AOP 把 Advice 细化了 BeforeAdvice、AfterAdvice、AfterReturningAdvice、ThrowsAdvice,目前我们做的测试案例中只用到了 BeforeAdvice,这部分可以对照 Spring 的源码进行补充测试。

实现步骤

1. 定义Advice拦截器链

BeforeAdvice (前置通知)

public interface BeforeAdvice extends Advice {
}

MethodBeforeAdvice

  • 在 Spring 框架中,Advice 都是通过方法拦截器 MethodInterceptor 实现的。环绕 Advice 类似一个拦截器的链路,Before Advice、After advice等,不过暂时我们需要那么多就只定义了一个 MethodBeforeAdvice 的接口定义。
public interface MethodBeforeAdvice extends BeforeAdvice {/*** 前置通知.* Callback before a given method is invoked.** @param method* @param args* @param target* @throws Throwable*/void before(Method method, Object[] args, Object target) throws Throwable;
}

2. 定义 Advisor 访问者

Advisor接口定义(顾问)

public interface Advisor {/*** 返回这个切面(Aspect)的通知部分。通知可能是一个拦截器(Interceptor),前置通知(Before Advice),抛出通知(Throws Advice)等** @return the advice that should apply if the pointcut matches.*/Advice getAdvice();
}

PointcutAdvisor

  • Advisor 承担了 Pointcut 和 Advice 的组合,Pointcut 用于获取 JoinPoint,而 Advice 决定于 JoinPoint 执行什么操作。
 */
public interface PointcutAdvisor extends Advisor {/*** Get the Pointcut that drives this advisor.*/Pointcut getPointcut();
}

AspectJExpressionPointcutAdvisor

  • AspectJExpressionPointcutAdvisor 实现了 PointcutAdvisor 接口,把切面 pointcut、拦截方法 advice 和具体的拦截表达式包装在一起。这样就可以在 xml 的配置中定义一个 pointcutAdvisor 切面拦截器了。
public class AspectJExpressionPointcutAdvisor implements PointcutAdvisor {// 切面private AspectJExpressionPointcut pointcut;// 具体的拦截方法private Advice advice;// 表达式private String expression;public void setExpression(String expression) {this.expression = expression;}@Overridepublic Advice getAdvice() {return advice;}public void setAdvice(Advice advice) {this.advice = advice;}@Overridepublic Pointcut getPointcut() {if (null == pointcut) {pointcut = new AspectJExpressionPointcut(expression);}return pointcut;}
}

3. 方法拦截器

  • MethodBeforeAdviceInterceptor 实现了 MethodInterceptor 接口,在 invoke 方法中调用 advice 中的 before 方法,传入对应的参数信息。
  • 而这个 advice.before 则是用于自己实现 MethodBeforeAdvice 接口后做的相应处理。其实可以看到具体的 MethodInterceptor 实现类,其实和我们之前做的测试是一样的,只不过现在交给了 Spring 来处理
public class MethodBeforeAdviceInterceptor implements MethodInterceptor {// 前置通知 拦截方法private MethodBeforeAdvice advice;public MethodBeforeAdviceInterceptor() {}public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {this.advice = advice;}@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {this.advice.before(methodInvocation.getMethod(), methodInvocation.getArguments(), methodInvocation.getThis());return methodInvocation.proceed();}
}

4. 代理工厂

ProxyFactory

  • 其实这个代理工厂主要解决的是关于 JDK 和 Cglib 两种代理的选择问题,有了代理工厂就可以按照不同的创建需求进行控制。
public class ProxyFactory {private AdvisedSupport advisedSupport;public ProxyFactory(AdvisedSupport advisedSupport) {this.advisedSupport = advisedSupport;}public Object getProxy() {return createAopProxy().getProxy();}private AopProxy createAopProxy() {if (advisedSupport.isProxyTargetClass()) {return new Cglib2AopProxy(advisedSupport);}return new JdkDynamicAopProxy(advisedSupport);}}

5. 融入Bean生命周期的自动代理创建者

DefaultAdvisorAutoProxyCreator

  • DefaultAdvisorAutoProxyCreator 类的主要核心实现在于 postProcessBeforeInstantiation 方法中,从通过 beanFactory.getBeansOfType 获取 AspectJExpressionPointcutAdvisor 开始。
  • 获取了 advisors 以后就可以遍历相应的 AspectJExpressionPointcutAdvisor 填充对应的属性信息,包括:目标对象、拦截方法、匹配器,之后返回代理对象即可。
  • 那么现在调用方获取到的这个 Bean 对象就是一个已经被切面注入的对象了,当调用方法的时候,则会被按需拦截,处理用户需要的信息。
public class DefaultAdvisorAutoProxyCreator implements InstantiationAwareBeanPostProcessor, BeanFactoryAware {private DefaultListableBeanFactory beanFactory;@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = (DefaultListableBeanFactory) beanFactory;}@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {if (isInfrastructureClass(beanClass)) {return null;}Collection<AspectJExpressionPointcutAdvisor> advisors = beanFactory.getBeansOfType(AspectJExpressionPointcutAdvisor.class).values();for (AspectJExpressionPointcutAdvisor advisor : advisors) {ClassFilter classFilter = advisor.getPointcut().getClassFilter();if (!classFilter.matches(beanClass)) {continue;}AdvisedSupport advisedSupport = new AdvisedSupport();TargetSource targetSource = null;try {targetSource = new TargetSource(beanClass.getDeclaredConstructor().newInstance());} catch (Exception e) {e.printStackTrace();}advisedSupport.setTargetSource(targetSource);advisedSupport.setMethodInterceptor((MethodInterceptor) advisor.getAdvice());advisedSupport.setMethodMatcher(advisor.getPointcut().getMethodMatcher());advisedSupport.setProxyTargetClass(false);return new ProxyFactory(advisedSupport).getProxy();}return null;}/*** 判断当前类是否为基础设施类: Advice/Pointcut/Advisor** @param beanClass* @return*/private boolean isInfrastructureClass(Class<?> beanClass) {return Advice.class.isAssignableFrom(beanClass)|| Pointcut.class.isAssignableFrom(beanClass)|| Advisor.class.isAssignableFrom(beanClass);}
}

测试

事先准备

IUserService接口.

public interface IUserService {String queryUserInfo();String register(String userName);}

UserService

  • 在 UserService 中提供了2个不同方法,另外你还可以增加新的类来加入测试。后面我们的测试过程,会给这个两个方法添加我们的拦截处理,打印方法执行耗时。
public class UserService implements IUserService {@Overridepublic String queryUserInfo() {try {Thread.sleep(new Random(1).nextInt(100));} catch (InterruptedException e) {e.printStackTrace();}return "小苏,111111,广州";}@Overridepublic String register(String userName) {try {Thread.sleep(new Random(1).nextInt(100));} catch (InterruptedException e) {e.printStackTrace();}return "注册用户: " + userName + " success!";}
}

注意pom中要引入额外的依赖。

   <dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.7</version><scope>test</scope></dependency><dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version></dependency><!-- https://mvnrepository.com/artifact/aopalliance/aopalliance --><dependency><groupId>aopalliance</groupId><artifactId>aopalliance</artifactId><version>1.0</version></dependency><!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.7</version></dependency><!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli --><dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-cli</artifactId><version>0.14</version></dependency>

自定义拦截方法

  • 与上一章节的拦截方法相比,我们不再是实现 MethodInterceptor 接口,而是实现 MethodBeforeAdvice 环绕拦截。在这个方法中我们可以获取到方法的一些信息,如果还开发了它的 MethodAfterAdvice 则可以两个接口一起实现。
public class UserServiceBeforeAdvice implements MethodBeforeAdvice {@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {System.out.println("前置拦截方法:" + method.getName());}
}

Spring.xml 配置AOP

  • 这回再使用 AOP 就可以像 Spring 中一样,通过在 xml 中配置即可。因为我们已经把 AOP 的功能融合到 Bean 的生命周期里去了,你的新增拦截方法都会被自动处理。
<?xml version="1.0" encoding="UTF-8"?>
<beans><bean id="userService" class="cn.suwg.springframework.test.bean.UserService"/><bean class="cn.suwg.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/><bean id="beforeAdvice" class="cn.suwg.springframework.test.bean.UserServiceBeforeAdvice"/><bean id="methodInterceptor" class="cn.suwg.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor"><property name="advice" ref="beforeAdvice"/></bean><bean id="pointcutAdvisor" class="cn.suwg.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor"><property name="expression" value="execution(* cn.suwg.springframework.test.bean.IUserService.*(..))"/><property name="advice" ref="methodInterceptor"/></bean></beans>

测试用例

  • 在单元测试中你只需要按照正常获取和使用 Bean 对象即可,不过这个时候如果被切面拦截了,那么其实你获取到的就是对应的代理对象里面的处理操作了。
public class ApiTest {@Testpublic void testAop() {ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");IUserService userService = applicationContext.getBean("userService", IUserService.class);System.out.println("测试结果:" + userService.queryUserInfo());}}

测试结果:

测试通过,从日志可以看到,通过代理方式、方法匹配和拦截后,在对应的目标方法下,做了拦截操作进行监控信息打印,内容都可以在控制台完整输出。

image-20241025195734886

image-20241025195756058

总结

  • 本章节实现 AOP 功能的外在体现主要是把以前自己在单元测试中的切面拦截,交给 Spring 的 xml 配置了,也就不需要自己手动处理了。那么这里有一个非常重要的知识点,就是把相应的功能如何与 Spring 的 Bean 生命周期结合起来,本章节用到的 BeanPostProcessor,因为它可以解决在 Bean 对象执行初始化方法之前,用于修改新实例化 Bean 对象的扩展点,所以我们也就可以处理自己的 AOP 代理对象逻辑了。

  • 一个功能的实现往往包括核心部分、组装部分、链接部分,为了这些各自职责的分工,则需要创建接口和类,由不同关系的继承、实现进行组装。只有明确了各个职责分工,才好灵活的扩展相应的功能逻辑,否则很难驾驭大型系统的开发和建设,也就是那种不好把握的感觉。

  • 目前我们实现的 AOP 与 Spring 源码中的核心逻辑是类似的,但更会偏简单一些,也不会考虑更多的复杂场景遇到的问题,包括是否有构造函数、是否为代理中的切面等。其实也可以看出只要是 Java 中的一些特性,都需要在真实使用的 Spring 中进行完整的实现,否则在使用这些功能的时候就会遇到各种问题。

参考书籍:《手写Spring渐进式源码实践》

书籍源代码:https://github.com/fuzhengwei/small-spring

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/58569.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Three.js 快速入门构建你的第一个 3D 应用

![ 开发领域&#xff1a;前端开发 | AI 应用 | Web3D | 元宇宙 技术栈&#xff1a;JavaScript、React、Three.js、WebGL、Go 经验经验&#xff1a;6年 前端开发经验&#xff0c;专注于图形渲染和AI技术 开源项目&#xff1a;github 晓智元宇宙、数字孪生引擎、前端面试题 大家好…

C#界面设计--9--fatal error C1083: 无法打开包括文件:“jruparse.h”: No such file or directory

1、VS2008-编译时报错“fatal error C1083: 无法打开包括文件:“jruparse.h”: No such file or directory” 2、问题出现的原因及解决方法 1、如果要引入的这些,h文件跟.cpp在同一个目录下&#xff0c;就不会出现这种问题&#xff0c;检査在工程的include目录下是不是真的存…

算法:排序

排序算法 1. 简单排序1.1 直接插入排序1.2 冒泡排序1.3 简单选择排序 2. 希尔排序3. 快速排序4. 堆排序5. 归并排序 将文件的内容按照某种规则进行排列。 排序算法的稳定判定&#xff1a;若在待排序的一个序列中&#xff0c; R i R_i Ri​和 R j R_j Rj​的关键码相同&#xf…

OpenCV视觉分析之目标跟踪(6)轻量级目标跟踪器类TrackerNano的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 Nano 跟踪器是一个超轻量级的基于深度神经网络&#xff08;DNN&#xff09;的通用目标跟踪器。 由于特殊的模型结构&#xff0c;Nano 跟踪器速度…

【新人系列】Python 入门(六):基础内容 - 上

✍ 个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12801353.html &#x1f4e3; 专栏定位&#xff1a;为 0 基础刚入门 Python 的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们…

WPF+MVVM案例实战(七)- 系统初始化界面字体描边效果实现

文章目录 1、案例效果展示2、项目准备3、功能实现1、资源获取2、界面代码3、后台代码4 源代码获取1、案例效果展示 2、项目准备 打开项目 Wpf_Examples,新建系统初始化界面 WelcomeWindow.xmal,如下所示: 3、功能实现 1、资源获取 案例中使用的CSDN文字为路径文字,从字体…

MFC工控项目实例二十七添加产品参数

承接专栏《MFC工控项目实例二十六创建数据库》 在型号参数界面添加三个参数试验时间、最小值、最大值。变量为double m_edit_time; double m_edit_min; double m_edit_max; 1、在SEAL_PRESSURE.h中添加代码 class CProductPara { public:union{struct{...double m_edit_min;…

【02】ZooKeeper经典应用场景实战一

1、ZooKeeper Java客户端实战 ZooKeeper应用的开发主要通过Java客户端API去连接和操作ZooKeeper集群。可供选择的Java客户端API有&#xff1a; ZooKeeper官方的Java客户端API。第三方的Java客户端API&#xff0c;比如&#xff1a;Curator ZooKeeper官方的客户端API提供了基本的…

信息安全工程师(73)网络安全风险评估过程

一、确定评估目标 此阶段需要明确评估的范围、目标和要求。评估目标通常包括特定的网络系统、信息系统或网络基础设施&#xff0c;评估范围可能涉及整个组织或仅特定部门。明确评估要求有助于确保评估过程的针对性和有效性。 二、收集信息 在评估开始之前&#xff0c;需要对目标…

Vmos pro-虚拟机 解锁永久vip

[应用名称] 应用名称&#xff1a;Vmos pro [应用版本] 应用版本&#xff1a;2.99 [软件大小] 软件大小&#xff1a;32.2mb [应用简介] 应用简介&#xff1a;Vmos Pro这款安卓虚拟机平台&#xff0c;提供了多样化的ROM版本选择。用户可根据自身需求更换ROM&#xff0c;调…

华为OD机试 - 最多购买宝石数目 - 滑动窗口(Python/JS/C/C++ 2024 C卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

1:基本电路专题:R(电阻)的介绍

说实话这个其实我不想写的&#xff0c;因为这个是初中的知识&#xff0c;并没有很难&#xff0c;但是为了保持整齐性&#xff0c;我还是写了一下关于这个的知识点。是电子学中三大基本无源元件之一。&#xff08;R&#xff08;电阻&#xff09;,L&#xff08;电感&#xff09;,…

基于SpringBoot的“CSGO赛事管理系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“CSGO赛事管理系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统首页界面图 赛事信息界面图 赛事通知界面…

漏洞挖掘 | 通过域混淆绕过实现账户接管

由于这是一个私有项目&#xff0c;我将使用 example.com 来代替。 很长一段时间以来&#xff0c;我一直想在漏洞赏金项目中找到一个账户接管&#xff08;ATO&#xff09;漏洞。于是&#xff0c;我开始探索项目范围内的 account.example.com。 我做的第一件事就是注册一个新账…

视觉目标检测标注xml格式文件解析可视化 - python 实现

视觉目标检测任务&#xff0c;通常用 labelimage标注&#xff0c;对应的标注文件为xml。 该示例来源于开源项目&#xff1a;https://gitcode.com/DataBall/DataBall-detections-100s/overview 读取 xml 标注文件&#xff0c;并进行可视化示例如下&#xff1a; #-*-coding:ut…

地理征服营销与开源 AI 智能名片 2 + 1 链动模式 S2B2C 商城小程序的融合创新

摘要&#xff1a;本文探讨了地理征服营销这一创新营销策略与开源 AI 智能名片 2 1 链动模式 S2B2C 商城小程序的融合应用。首先阐述地理征服营销的概念和实施要点&#xff0c;接着介绍开源 AI 智能名片 2 1 链动模式 S2B2C 商城小程序的功能与优势&#xff0c;分析二者结合如…

三周精通FastAPI:24 OAuth2 实现简单的 Password 和 Bearer 验证

官网文档&#xff1a;https://fastapi.tiangolo.com/zh/tutorial/security/simple-oauth2/ OAuth2 实现简单的 Password 和 Bearer 验证 本章添加上一章示例中欠缺的部分&#xff0c;实现完整的安全流。 获取 username 和 password 首先&#xff0c;使用 FastAPI 安全工具获…

字节青训-兔群繁殖之谜

问题描述 生物学家小 R 正在研究一种特殊的兔子品种的繁殖模式。这种兔子的繁殖遵循以下规律&#xff1a; 每对成年兔子每个月会生育一对新的小兔子&#xff08;一雌一雄&#xff09;。新生的小兔子需要一个月成长&#xff0c;到第二个月才能开始繁殖。兔子永远不会死亡。 小 R…

MiniWord

1.nuget 下载配置 2.引用 3. var value = new Dictionary<string, object>() { ["nianfen"] = nianfen, ["yuefen"] = yuefen, ["yuefenjian1"] = (int.Par…

计算机毕业设计Python+大模型恶意木马流量检测与分类 恶意流量监测 随机森林模型 深度学习 机器学习 数据可视化 大数据毕业设计 信息安全 网络安全

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; Python大模型恶意木马流量检…