手撕spring bean的加载过程

这里我们采用手撕源码的方式,开始探索spring boot源码中最有意思的部分-bean的生命周期,也可以通过其中的原理理解很多面试以及工作中偶发遇到的问题。

springboot基于约定大于配置的思想对spring进行优化,使得这个框架变得更加轻量化,集成各种starter组件时使其能够更加全面。

1、SpringApplication启动类的配置与软件包的反射加载

通常我们在建立一个新的spring boot项目时,利用idea脚手架生成模板内部会自带一个标注有SpringApplication注解的启动类,如下所示:

/*** @author : spring* {@code @description:}* {@code @date} : 2024/2/4* {@code @modified} By: spring* {@code @project:} spring-plus*/
@SpringApplication(scanBeanPackagePath = "com/hlc/springplus/test")
public class ApplicationStarter {public static void main(String[] args) {ApplicationContext applicationContext = new DefaultApplicationContext(ApplicationStarter.class);TestBean contextBean = (TestBean) applicationContext.getBean("test");contextBean.test();}
}

由于本文的代码全部都是手撕,所以会与spring boot的源码有所不同,个人主义完美凸显。

言回正传,用过spring boot的人都知道它主要的特色基础是它的“容器”的概念,我们可以通过配置文件、注解、导入以及反射实例化后调用通用应用上下文注入的方式将我们的bean交给spring容器管理,那么这里启动类启动后“容器”是怎么识别出我们标准或配置的bean信息同时将其实例化、配置属性、配置名称.....的呢?

那么下面就是一个过程

获取启动类上标注注解中的包路径值
SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);String beanPackagePath = annotation.scanBeanPackagePath();
String path = beanPackagePath.replace('.', '/');

这里的path就是我们实际的包路径,为什么需要将.替换城/呢?实际上我们配置的包路径是软件包中的相对路径,并不是Resource获取时规定的路径格式。

获取当前类的类加载器并根据路径加载URL获取文件
 ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();URL resource = classLoader.getResource(path);File file = new File(resource.getFile());

DefaultApplicationContext就是我当前类的名称,后续串完全部的流程会将全部的代码挂出的,这里的类加载器获取资源的方式是比较常用的。

需要注意的是这里拿到的file有可能是文件夹,也可能是文件。

通过文件夹或文件夹获取字节码
 for (File item : files) {int begin = item.getAbsolutePath().indexOf("com");int end = item.getAbsolutePath().indexOf('.');String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');try {Class<?> clazz = Class.forName(className);if (clazz.isAnnotationPresent(Component.class)) {beanClazzList.add(clazz);//收集后置处理器(意图是收集后置处理器而不是收集bean对象)if (BeanPostprocessor.class.isAssignableFrom(clazz)) {beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());}}} catch (Exception e) {throw new RuntimeException(e.getMessage());}                            }

上面的类路径获取方式以及判断字节码是否实现了接口BeanPostprocessor的判断、字节码是否标注了注解Component的判断都是比较常用的方法。

那么通过此三步就将全部需要加载的字节码文件都获取到我们的成员变量beanClazzList列表中去了。

2、ApplicationContext接口的定义以及相关注解的配置

虽然我们解决了待加载bean的字节码列表的收集问题,但是spring boot的容器我们还没有加载出来,也没有实现相关注解的配置,注解标注了bean的身份、名称、类型、加载方式、加载条件、加载顺序、依赖关系等。

ApplicationContext接口的定义
public interface ApplicationContext extends BeanFactory {Object getBean(String beanName);<T> void registerBean(T bean, Class<T> clazz, String name);
}

ApplicationContext接口的释义是“应用上下文”,在计算机科学中,上下文表示进程在执行过程中系统内部的资源情况与中断向量表的记录情况,总之代表的是进程所处的逻辑环境。这里顾名思义ApplicationContext代表的也自然就是bean所处的环境。也就是我们口中的spring boot的容器。

对于基本的ApplicationContext能力而言,它应当具备获取bean对象与注册bean对象的能力。所以这里定义的两个基础能力接口。在考虑如何实现它之前,我们还需要配置以下bean相关的其他注解:

组件注解(标注类型为bean组件)

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {String name() default "";
}

注入注解(标注为某类型注入某bean的属性值)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {String value() default "";
}

作用域注解(标注单例、原型等生存周期的bean类型)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {String value();
}

懒加载注解(标注即用即加载还是立刻加载)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {
}

初始化方法注解(标注bean的初始化方法)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitBeanMethod {
}

3、实现ApplicationContext接口与bean加载原理

上述我们定义了相关接口与注解,接下来我们实现容器上下文接口以及描述bean是如何加载到容器内部管理的。

那么这里我们就先将实现ApplicationContext接口的DefaultApplicationContext.java代码放在下方:

public class DefaultApplicationContext implements ApplicationContext {private final Class<?> appconfig;private List<Class<?>> beanClazzList = new LinkedList<>();private Map<String, Object> singletonBeanMap = new ConcurrentHashMap<>();private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();private List<BeanPostprocessor> beanPostprocessorList = new LinkedList<>();public DefaultApplicationContext(Class<?> appconfig) {this.appconfig = appconfig;//1、扫描启动类注解的字节码列表scanBeansByPackage(beanClazzList);//2、注册bean的BeanDefinition初始配置信息initBeanDefinition(beanClazzList, beanDefinitionMap);//3、实例化单例bean并存入map中instanceSingletonBeans(beanDefinitionMap, singletonBeanMap);}@Overridepublic Object getBean(String beanName) {BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);if (null != beanDefinition && "prototype".equals(beanDefinition.getScope())) {try {return beanDefinition.getBeanClazz().getDeclaredConstructor().newInstance();} catch (InstantiationException | IllegalAccessException | InvocationTargetException |NoSuchMethodException e) {throw new RuntimeException(e.getMessage());}}return singletonBeanMap.get(beanName);}@Overridepublic <T> void registerBean(T bean, Class<T> clazz, String name) {singletonBeanMap.put(name, bean);}/*** 扫描bean的字节码列表** @param beanClazzList bean的字节码列表(待填充)*/protected void scanBeansByPackage(List<Class<?>> beanClazzList) {if (null != appconfig && appconfig.isAnnotationPresent(SpringApplication.class)) {SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);if (null != annotation) {String beanPackagePath = annotation.scanBeanPackagePath();String path = beanPackagePath.replace('.', '/');ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();URL resource = classLoader.getResource(path);if (resource != null) {File file = new File(resource.getFile());if (file.isDirectory()) {File[] files = file.listFiles();if (files != null) {for (File item : files) {loadAndFilterBeanClazzes(beanClazzList, item);}}} else {loadAndFilterBeanClazzes(beanClazzList, file);}}} else {throw new RuntimeException("Annotation SpringApplication is not exist");}} else {throw new RuntimeException("Annotation SpringApplication is not exist and appconfig is null");}}/*** 加载bean的字节码列表并过滤** @param beanClazzList bean的字节码列表(待填充)* @param item          文件或文件夹*/private void loadAndFilterBeanClazzes(List<Class<?>> beanClazzList, File item) {int begin = item.getAbsolutePath().indexOf("com");int end = item.getAbsolutePath().indexOf('.');String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');try {Class<?> clazz = Class.forName(className);if (clazz.isAnnotationPresent(Component.class)) {beanClazzList.add(clazz);//收集后置处理器(意图是收集后置处理器而不是收集bean对象)if (BeanPostprocessor.class.isAssignableFrom(clazz)) {beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());}}} catch (Exception e) {throw new RuntimeException(e.getMessage());}}/*** 注册bean的BeanDefinition初始配置信息** @param beanClazzList     bean的字节类型列表* @param beanDefinitionMap bean的BeanDefinition初始配置信息池子*/private void initBeanDefinition(List<Class<?>> beanClazzList, Map<String, BeanDefinition> beanDefinitionMap) {if (null != beanClazzList && !beanClazzList.isEmpty()) {for (Class<?> clazz : beanClazzList) {BeanDefinition beanDefinition = new BeanDefinition();Component component = clazz.getAnnotation(Component.class);Scope scope = clazz.getAnnotation(Scope.class);Lazy lazy = clazz.getAnnotation(Lazy.class);beanDefinition.setBeanClazz(clazz);beanDefinition.setLazy(null != lazy);beanDefinition.setScope(null != scope ? scope.value() : "prototype");String beanName = component.name();if (beanName.isEmpty()) {beanName = clazz.getSimpleName();}beanDefinitionMap.put(beanName, beanDefinition);}}}/*** 实例化单例bean** @param beanDefinitionMap bean定义信息* @param singletonBeanMap  单例bean池子*/private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {continue;}if (!clazz.isAnnotationPresent(Lazy.class)) {//实例化beantry {Component component = clazz.getAnnotation(Component.class);String beanName = component.name();if (null == beanName || beanName.isEmpty()) {beanName = clazz.getSimpleName();}//1、实例化beanObject newInstance = clazz.getDeclaredConstructor().newInstance();//2、属性填充attributeAutoWiredPadding(clazz, newInstance);//3、aware能力透传awareBeanInstancePadding(newInstance);//4、初始化//4.1、后置处理器 初始化前执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);}//4.2、初始化bean执行//检查是否实现初始化Bean的接口initializeBeanInstancePadding(newInstance);//检查是否配置过init方法initBeanMethodInstancePadding(newInstance);//4.3、后置处理器能力 初始化后执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);}singletonBeanMap.put(beanName, newInstance);} catch (InvocationTargetException | NoSuchMethodException | InstantiationException |IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}/*** bean的属性填充** @param beanClazz   bean的字节类型* @param newInstance 实例化的bean*/private void attributeAutoWiredPadding(Class<?> beanClazz, Object newInstance) {if (null != beanClazz) {Field[] fields = beanClazz.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(AutoWired.class)) {field.setAccessible(true);Class<?> declaringClass = field.getType();AutoWired autoWired = field.getAnnotation(AutoWired.class);String name = autoWired.value();if (null == name || name.isEmpty()) {name = declaringClass.getSimpleName();}Object fieldBean = singletonBeanMap.get(name);if (null == fieldBean) {List<Class<?>> beanClazzList = new LinkedList<>();beanClazzList.add(declaringClass);initBeanDefinition(beanClazzList, beanDefinitionMap);Map<String, BeanDefinition> definitionMap = new HashMap<>();definitionMap.put(name, beanDefinitionMap.get(name));instanceSingletonBeans(definitionMap, singletonBeanMap);try {field.set(newInstance, singletonBeanMap.get(name));} catch (IllegalAccessException e) {throw new RuntimeException(e.getMessage());}} else {try {field.set(newInstance, fieldBean);} catch (IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}}/*** bean的Aware接口的实现类填充** @param bean bean实例对象*/private void awareBeanInstancePadding(Object bean) {if (null != bean) {if (bean instanceof Aware) {if (bean instanceof ApplicationContextAware) {((ApplicationContextAware) bean).setApplicationContext(this);}if (bean instanceof BeanNameAware) {((BeanNameAware) bean).setBeanName();}}}}/*** bean的初始化方法填充** @param bean 实例化的bean*/private void initializeBeanInstancePadding(Object bean) {if (null != bean) {if (bean instanceof InitializingBean) {((InitializingBean) bean).afterPropertiesSet();}}}/*** bean的初始化方法填充** @param newInstance 实例化的bean*/private void initBeanMethodInstancePadding(Object newInstance) {if (null != newInstance) {Method[] methods = newInstance.getClass().getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(InitBeanMethod.class)) {method.setAccessible(true);try {method.invoke(newInstance);} catch (IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e.getMessage());}}}}}private void sortBeanInstanceClazzList() {}
}

这里我们着重描述一下关于bean加载原理的这一块代码:

private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {continue;}if (!clazz.isAnnotationPresent(Lazy.class)) {//实例化beantry {Component component = clazz.getAnnotation(Component.class);String beanName = component.name();if (null == beanName || beanName.isEmpty()) {beanName = clazz.getSimpleName();}//1、实例化beanObject newInstance = clazz.getDeclaredConstructor().newInstance();//2、属性填充attributeAutoWiredPadding(clazz, newInstance);//3、aware能力透传awareBeanInstancePadding(newInstance);//4、初始化//4.1、后置处理器 初始化前执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);}//4.2、初始化bean执行//检查是否实现初始化Bean的接口initializeBeanInstancePadding(newInstance);//检查是否配置过init方法initBeanMethodInstancePadding(newInstance);//4.3、后置处理器能力 初始化后执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);}singletonBeanMap.put(beanName, newInstance);} catch (InvocationTargetException | NoSuchMethodException | InstantiationException |IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}

到这里我们已经了解在获取待加载bean的字节码列表之后,我们需要将bean的配置信息存储到我们的beanDefinitionMap中,再根据beanDefinitionMap将其中的单例bean信息加载成一个个bean放入单例bean map中,这里的存储key统一都是beanName。

看以上代码我们不难分析出通过bean配置信息加载bean的过程中,一个bean需要经过6步周期性工作才会被放入容器中给我们使用。以下是图示:

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

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

相关文章

【win】vscode无法使用ctrl+shift+p快捷键的解决方案

本文首发于 ❄️慕雪的寒舍 今天使用vscode的时候遇到的这个问题&#xff0c;明明快捷键设置的是ctrlshiftp&#xff0c;但是在电脑上怎么敲都敲不出来&#xff0c;因为用这个快捷键打开命令面板都习惯了&#xff0c;也不想换&#xff0c;就在找原因。 同时百度的时候还遇到了…

vue懒加载请求思路

当页面中不存在分页时&#xff0c;首先考虑到的就是懒加载&#xff0c;所以今天提供一个懒加载的思路。 首先是是么时候应该触发懒加载&#xff0c;以上面页面为例当页面容器中的卡片不能充满屏幕时就会触发加载出新数据&#xff0c;触发前提是1.已获取数据并非全部的。2.上一次…

Vitest 单元测试详解

一、自动化测试&#xff08;TDD&#xff09;的一些概念&#xff1a; 自动化测试&#xff08;TDD&#xff09;概念&#xff1a; 自动化测试是指 使用独立于待测软件的其他软件或程序来自动执行测试&#xff0c;比较实际结果与预期 并生成测试报告这一过程。在测试流程已经确定后…

Python:批量url链接保存为PDF

我的数据是先把url链接获取到存入excel中&#xff0c;后续对excel做的处理&#xff0c;各位也可以直接在程序中做处理&#xff0c;下面就是针对excel中的链接做批量处理 excel内容格式如下&#xff08;涉及具体数据做了隐藏&#xff09; 标题文件链接文件日期网页标题1http://…

C++二维数组

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 大家好&#xff0c;我是PingdiGuo_guo&#xff0c;今天我们来学习二维数组。 文章目录 1.二维数组的概念与思想 2.二维数组和一维数组的区别 3.二维数组的特点 4.二维数组的操作 1.定义 2.初始化 1.直…

百面嵌入式专栏(面试题)进程管理相关面试题1.0

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍进程管理相关面试题 。 一、进程管理相关面试题 进程是什么?操作系统如何描述和抽象一个进程?进程是否有生命周期?如何标识一个进程?进程与进程之间的关系如何?Linux操作系统的进程0是什么?Linux操…

【Linux】线程Pthread的概念 | NPTL线程库函数

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;Linux系列专栏&#xff1a;Linux基础 &#x1f525; 给大家…

假期刷题打卡--Day25

1、MT1204字母三角 请编写一个简单程序&#xff0c;输入正整数n&#xff0c;输出n行F字三角 格式 输入格式&#xff1a; 输入整型 输出格式&#xff1a; 输出n行F字三角 样例 1 输入&#xff1a; 5输出&#xff1a; F FFF FFFFF FFFFFFF FFFFFFFFF 分析过程…

CSS:两列布局

两列布局是指一列宽度固定&#xff0c;另一列自适应。效果如下&#xff1a; HTML: <div class"container clearfix"><div class"left"></div><div class"right"></div> </div>公共 CSS&#xff1a; .con…

结构体的深入了解(下)

1.修改默认对齐数 在之前我们了解到在vs中的默认对齐数为8&#xff0c;在gcc下没有默认对齐数的&#xff0c;那我们能否在vs上进行修改呢&#xff1f;我们来试一下&#xff1a;&#xff08;#pragma 这个预处理指令&#xff0c;可以改变编译器的默认对齐数&#xff09; #includ…

信号系统之滤波详解

1 过滤的基础 通常希望使用信号的幅度&#xff0c;而不是它的功率。例如&#xff0c;假设一个增益为20dB的放大器。根据定义&#xff0c;这意味着信号中的功率增加了 100 倍。由于幅度与功率的平方根成正比&#xff0c;因此输出幅度是输入幅度的 10 倍。虽然 20dB 意味着功率的…

基于SSM的实习管理系统(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的实习管理系统&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring Spri…