图文并茂,揭秘 Spring 的 Bean 的加载过程

图文并茂,揭秘 Spring 的 Bean 的加载过程,第1张

目录

Spring 作为 Ioc 框架,实现了依赖注入,由一个中心化的 Bean 工厂来负责各个 Bean 的实例化和依赖管理。各个 Bean 可以不需要关心各自的复杂的创建过程,达到了很好的解耦效果。

我们对 Spring 的工作流进行一个粗略的概括,主要为两大环节:

我们假设所有的配置和扩展类都已经装载到了 ApplicationContext 中,然后具体的分析一下 Bean 的加载流程。

思考一个问题,抛开 Spring 框架的实现,假设我们手头上已经有一套完整的 Bean Definition Map,然后指定一个 beanName 要进行实例化,需要关心什么?即使我们没有 Spring 框架,也需要了解这两方面的知识:

Spring 进行了抽象和封装,使得作用域和依赖关系的配置对开发者透明,我们只需要知道当初在配置里已经明确指定了它的生命周期和依赖了谁,至于是怎么实现的,依赖如何注入,托付给了 Spring 工厂来管理。

Spring 只暴露了很简单的接口给调用者,比如 getBean :

那我们就从 getBean 方法作为入口,去理解 Spring 加载的流程是怎样的,以及内部对创建信息、作用域、依赖关系等等的处理细节。

上面是跟踪了 getBean 的调用链创建的流程图,为了能够很好地理解 Bean 加载流程,省略一些异常、日志和分支处理和一些特殊条件的判断。

从上面的流程图中,可以看到一个 Bean 加载会经历这么几个阶段(用绿色标记):

整个流程最为复杂的是对循环依赖的解决方案,后续会进行重点分析。

而在我们解析完配置后创建的 Map,使用的是 beanName 作为 key。见 DefaultListableBeanFactory:

BeanFactorygetBean 中传入的 name,有可能是这几种情况:

为了能够获取到正确的 BeanDefinition,需要先对 name 做一个转换,得到 beanName。

见 AbstractBeanFactorydoGetBean :

如果是 alias name ,在解析阶段,alias name 和 bean name 的映射关系被注册到 SimpleAliasRegistry 中。从该注册器中取到 beanName。见 SimpleAliasRegistrycanonicalName :

如果是 factorybean name ,表示这是个工厂 bean,有携带前缀修饰符 & 的,直接把前缀去掉。见 BeanFactoryUtilstransformedBeanName :

我们从配置文件读取到的 BeanDefinition 是 GenericBeanDefinition 。它的记录了一些当前类声明的属性或构造参数,但是对于父类只用了一个 parentName 来记录。

接下来会发现一个问题,在后续实例化 Bean 的时候,使用的 BeanDefinition 是 RootBeanDefinition 类型而非 GenericBeanDefinition 。这是为什么?

答案很明显,GenericBeanDefinition 在有继承关系的情况下,定义的信息不足:

为了能够正确初始化对象,需要完整的信息才行 。需要递归 合并父类的定义

见 AbstractBeanFactorydoGetBean :

在判断 parentName 存在的情况下,说明存在父类定义,启动合并。如果父类还有父类怎么办?递归调用,继续合并。

见 AbstractBeanFactorygetMergedBeanDefinition 方法:

每次合并完父类定义后,都会调用 RootBeanDefinitionoverrideFrom 对父类的定义进行覆盖,获取到当前类能够正确实例化的 全量信息

什么是循环依赖?

举个例子,这里有三个类 A、B、C,然后 A 关联 B,B 关联 C,C 又关联 A,这就形成了一个循环依赖。如果是方法调用是不算循环依赖的,循环依赖必须要持有引用。

循环依赖根据注入的时机分成两种类型:

如果是构造器循环依赖,本质上是无法解决的 。比如我们准调用 A 的构造器,发现依赖 B,于是去调用 B 的构造器进行实例化,发现又依赖 C,于是调用 C 的构造器去初始化,结果依赖 A,整个形成一个死结,导致 A 无法创建。

如果是设值循环依赖,Spring 框架只支持单例下的设值循环依赖 。Spring 通过对还在创建过程中的单例,缓存并提前暴露该单例,使得其他实例可以引用该依赖。

Spring 不支持原型模式的任何循环依赖 。检测到循环依赖会直接抛出 BeanCurrentlyInCreationException 异常。

使用了一个 ThreadLocal 变量 prototypesCurrentlyInCreation 来记录当前线程正在创建中的 Bean 对象,见 AbtractBeanFactory#prototypesCurrentlyInCreation :

在 Bean 创建前进行记录,在 Bean 创建后删除记录。见 AbstractBeanFactorydoGetBean :

见 AbtractBeanFactorybeforePrototypeCreation 的记录 *** 作:

见 AbtractBeanFactorybeforePrototypeCreation 的删除 *** 作:

为了节省内存空间,在单个元素时 prototypesCurrentlyInCreation 只记录 String 对象,在多个依赖元素后改用 Set 集合。这里是 Spring 使用的一个节约内存的小技巧。

了解了记录的写入和删除过程好了,再来看看读取以及判断循环的方式。这里要分两种情况讨论。

这两个地方的实现略有不同。

如果是构造函数依赖的,比如 A 的构造函数依赖了 B,会有这样的情况。实例化 A 的阶段中,匹配到要使用的构造函数,发现构造函数有参数 B,会使用 BeanDefinitionValueResolver 来检索 B 的实例。见 BeanDefinitionValueResolverresolveReference :

我们发现这里继续调用 beanFactorygetBean 去加载 B。

如果是设值循环依赖的的,比如我们这里不提供构造函数,并且使用了 @Autowire 的方式注解依赖(还有其他方式不举例了):

加载过程中,找到无参数构造函数,不需要检索构造参数的引用,实例化成功。接着执行下去,进入到属性填充阶段 AbtractBeanFactorypopulateBean ,在这里会进行 B 的依赖注入。

为了能够获取到 B 的实例化后的引用,最终会通过检索类 DependencyDescriptor 中去把依赖读取出来,见 DependencyDescriptorresolveCandidate :

发现 beanFactorygetBean 方法又被调用到了。

在这里,两种循环依赖达成了同一 。无论是构造函数的循环依赖还是设置循环依赖,在需要注入依赖的对象时,会继续调用 beanFactorygetBean 去加载对象,形成一个递归 *** 作。

而每次调用 beanFactorygetBean 进行实例化前后,都使用了 prototypesCurrentlyInCreation 这个变量做记录。按照这里的思路走,整体效果等同于 建立依赖对象的构造链

prototypesCurrentlyInCreation 中的值的变化如下:

调用判定的地方在 AbstractBeanFactorydoGetBean 中,所有对象的实例化均会从这里启动。

判定的实现方法为 AbstractBeanFactoryisPrototypeCurrentlyInCreation :

所以在原型模式下,构造函数循环依赖和设值循环依赖,本质上使用同一种方式检测出来。Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。

Spring 也不支持单例模式的构造循环依赖 。检测到构造循环依赖也会抛出 BeanCurrentlyInCreationException 异常。

和原型模式相似,单例模式也用了一个数据结构来记录正在创建中的 beanName。见 DefaultSingletonBeanRegistry :

会在创建前进行记录,创建化后删除记录。

见 DefaultSingletonBeanRegistrygetSingleton

记录和判定的方式见 DefaultSingletonBeanRegistrybeforeSingletonCreation :

这里会尝试往 singletonsCurrentlyInCreation 记录当前实例化的 bean。我们知道 singletonsCurrentlyInCreation 的数据结构是 Set,是不允许重复元素的, 所以一旦前面记录了,这里的 add *** 作将会返回失败

比如加载 A 的单例,和原型模式类似,单例模式也会调用匹配到要使用的构造函数,发现构造函数有参数 B,然后使用 BeanDefinitionValueResolver 来检索 B 的实例,根据上面的分析,继续调用 beanFactorygetBean 方法。

所以拿 A,B,C 的例子来举例 singletonsCurrentlyInCreation 的变化,这里可以看到和原型模式的循环依赖判断方式的算法是一样:

单例模式下,构造函数的循环依赖无法解决,但设值循环依赖是可以解决的

这里有一个重要的设计: 提前暴露创建中的单例

我们理解一下为什么要这么做。

还是拿上面的 A、B、C 的的设值依赖做分析,

=> 1 A 创建 -> A 构造完成,开始注入属性,发现依赖 B,启动 B 的实例化

=> 2 B 创建 -> B 构造完成,开始注入属性,发现依赖 C,启动 C 的实例化

=> 3 C 创建 -> C 构造完成,开始注入属性,发现依赖 A

重点来了,在我们的阶段 1中, A 已经构造完成,Bean 对象在堆中也分配好内存了,即使后续往 A 中填充属性(比如填充依赖的 B 对象),也不会修改到 A 的引用地址。

所以,这个时候是否可以 提前拿 A 实例的引用来先注入到 C ,去完成 C 的实例化,于是流程变成这样。

=> 3 C 创建 -> C 构造完成,开始注入依赖,发现依赖 A,发现 A 已经构造完成,直接引用,完成 C 的实例化。

=> 4 C 完成实例化后,B 注入 C 也完成实例化,A 注入 B 也完成实例化。

这就是 Spring 解决单例模式设值循环依赖应用的技巧。流程图为:

为了能够实现单例的提前暴露。Spring 使用了三级缓存,见 DefaultSingletonBeanRegistry :

这三个缓存的区别如下:

从 getBean("a") 开始,添加的 SingletonFactory 具体实现如下:

可以看到如果使用该 SingletonFactory 获取实例,使用的是 getEarlyBeanReference 方法,返回一个未初始化的引用。

读取缓存的地方见 DefaultSingletonBeanRegistry :

先尝试从 singletonObjects 和 singletonFactory 读取,没有数据,然后尝试 singletonFactories 读取 singletonFactory,执行 getEarlyBeanReference 获取到引用后,存储到 earlySingletonObjects 中。

这个 earlySingletonObjects 的好处是,如果此时又有其他地方尝试获取未初始化的单例,可以从 earlySingletonObjects 直接取出而不需要再调用 getEarlyBeanReference 。

从流程图上看,实际上注入 C 的 A 实例,还在填充属性阶段,并没有完全地初始化。等递归回溯回去,A 顺利拿到依赖 B,才会真实地完成 A 的加载。

获取到完整的 RootBeanDefintion 后,就可以拿这份定义信息来实例具体的 Bean。

具体实例创建见 AbstractAutowireCapableBeanFactorycreateBeanInstance ,返回 Bean 的包装类 BeanWrapper,一共有三种策略:

使用工厂方法创建,会先使用 getBean 获取工厂类,然后通过参数找到匹配的工厂方法,调用实例化方法实现实例化,具体见 ConstructorResolverinstantiateUsingFactoryMethod :

使用有参构造函数创建,整个过程比较复杂,涉及到参数和构造器的匹配。为了找到匹配的构造器,Spring 花了大量的工作,见 ConstructorResolverautowireConstructor :

使用无参构造函数创建是最简单的方式,见 AbstractAutowireCapableBeanFactoryinstantiateBean :

我们发现这三个实例化方式,最后都会走 getInstantiationStrategy()instantiate() ,见实现类 SimpleInstantiationStrategyinstantiate :

虽然拿到了构造函数,并没有立即实例化。因为用户使用了 replace 和 lookup 的配置方法,用到了动态代理加入对应的逻辑。如果没有的话,直接使用反射来创建实例。

创建实例后,就可以开始注入属性和初始化等 *** 作。

但这里的 Bean 还不是最终的 Bean。返回给调用方使用时,如果是 FactoryBean 的话需要使用 getObject 方法来创建实例。见 AbstractBeanFactorygetObjectFromBeanInstance ,会执行到 doGetObjectFromFactoryBean :

实例创建完后开始进行属性的注入,如果涉及到外部依赖的实例,会自动检索并关联到该当前实例。

Ioc 思想体现出来了。正是有了这一步 *** 作,Spring 降低了各个类之间的耦合。

属性填充的入口方法在 AbstractAutowireCapableBeanFactorypopulateBean 。

可以看到主要的处理环节有:

如果我们的 Bean 需要容器的一些资源该怎么办?比如需要获取到 BeanFactory、ApplicationContext 等等。

Spring 提供了 Aware 系列接口来解决这个问题。比如有这样的 Aware:

Spring 在初始化阶段,如果判断 Bean 实现了这几个接口之一,就会往 Bean 中注入它关心的资源。

见 AbstractAutowireCapableBeanFactoryinvokeAwareMethos :

在 Bean 的初始化前或者初始化后,我们如果需要进行一些增强 *** 作怎么办?

这些增强 *** 作比如打日志、做校验、属性修改、耗时检测等等。Spring 框架提供了 BeanPostProcessor 来达成这个目标。比如我们使用注解 @Autowire 来声明依赖,就是使用 AutowiredAnnotationBeanPostProcessor 来实现依赖的查询和注入的。接口定义如下:

实现该接口的 Bean 都会被 Spring 注册到 beanPostProcessors 中, 见 AbstractBeanFactory :

只要 Bean 实现了 BeanPostProcessor 接口,加载的时候会被 Spring 自动识别这些 Bean,自动注册,非常方便。

然后在 Bean 实例化前后,Spring 会去调用我们已经注册的 beanPostProcessors 把处理器都执行一遍。

这里使用了责任链模式,Bean 会在处理器链中进行传递和处理。当我们调用 BeanFactorygetBean 的后,执行到 Bean 的初始化方法 AbstractAutowireCapableBeanFactoryinitializeBean 会启动这些处理器。

自定义初始化有两种方式可以选择:

见 AbstractAutowireCapableBeanFactoryinvokeInitMethods :

Bean 已经加载完毕,属性也填充好了,初始化也完成了。

在返回给调用者之前,还留有一个机会对 Bean 实例进行类型的转换。见 AbstractBeanFactorydoGetBean :

抛开一些细节处理和扩展功能,一个 Bean 的创建过程无非是:

获取完整定义 -> 实例化 -> 依赖注入 -> 初始化 -> 类型转换。

作为一个完善的框架,Spring 需要考虑到各种可能性,还需要考虑到接入的扩展性。

所以有了复杂的循环依赖的解决,复杂的有参数构造器的匹配过程,有了 BeanPostProcessor 来对实例化或初始化的 Bean 进行扩展修改。

先有个整体设计的思维,再逐步击破针对这些特殊场景的设计,整个 Bean 加载流程迎刃而解。

利用java 反射机制,运行时获取类信息

package comtestdoemo;

import javalangreflectField;

import javalangreflectMethod;

public class MainTest {

/

@param args

/

public static void main(String[] args) throws Exception{

Class demo=ClassforName("comtestdoemoStudent");

Field[] fields=demogetDeclaredFields();

Method[] methods=demogetDeclaredMethods();

for(Field f:fields){

Systemoutprintln("字段:"+fgetName());

}

for(Method method:methods){

String m=methodgetName();

if( mcontains("set")){

Systemoutprintln("方法:"+m+"()");

}

}

}

}

class Student{

private String name;

private int age;

private String address;

public String getName() {

return name;

}

public void setName(String name) {

thisname = name;

}

public int getAge() {

return age;

}

public void setAge(int age) {

thisage = age;

}

public String getAddress() {

return address;

}

public void setAddress(String address) {

thisaddress = address;

}

}

pring配置中的bean直接引用其它bean的属性值来赋值给当前bean的属性,也可以直接调用其它bean的方法获取返回值来赋值给当前bean的属性,并且可以进行参数传递,这样可以省去在bean中注入需要获取属性值的bean。

以下是一个完整的示例:

1、需要JAVA类:

Spring配置中的bean直接引用其它bean的属性值

package comservicetest;

public class Bean1 {

int v1 = 1;

public int getV1() {

return v1;

}

public void setV1(int v1) {

thisv1 = v1;

}

public int getTestValue(int v1) {

return v1;

}

public String getTestValue2(String str) {

return str;

}

}

package comservicetest;

public class Bean2 {

int v1;

int v2;

int v3;

String v4Str;

public int getV1() {

return v1;

}

public void setV1(int v1) {

thisv1 = v1;

}

public int getV2() {

return v2;

}

public void setV2(int v2) {

thisv2 = v2;

}

public int getV3() {

return v3;

}

public void setV3(int v3) {

thisv3 = v3;

}

public String getV4Str() {

return v4Str;

}

public void setV4Str(String str) {

v4Str = str;

}

}

package comservicetest;

import orgspringframeworkcontextApplicationContext;

import orgspringframeworkcontextsupportClassPathXmlApplicationContext;

public class Test {

/

@param args

/

public static void main(String[] args) {

// TODO Auto-generated method stub

ApplicationContext context = new ClassPathXmlApplicationContext("testBeanxml");

Bean2 bean2 = (Bean2)contextgetBean("bean2");

Systemoutprintln(bean2getV1());

Systemoutprintln(bean2getV2());

Systemoutprintln(bean2getV3());

Systemoutprintln(bean2getV4Str());

}

}

2、需要XML配置:

<xml version="10" encoding="UTF-8">

<beans

xmlns=">

SSM全称是Spring+SpringMVC+MyBatis。

SSM框架集由Spring、MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容)。常作为数据源较简单的web项目的框架。

1、Spring

Spring就像是整个项目中装配bean的大工厂,在配置文件中可以指定使用特定的参数去调用实体类的构造方法来实例化对象。也可以称之为项目中的粘合剂。

2、SpringMVC

SpringMVC在项目中拦截用户请求,它的核心Servlet即DispatcherServlet承担中介或是前台这样的职责。

3、mybatis

mybatis是对jdbc的封装,它让数据库底层 *** 作变的透明。

SSM框架集是软件架构的一个部分。以下是软件架构的种类:

1、逻辑架构

软件系统系统当中的各个元件之间所存在的关系,比如外部系统接口、用户界面、商业逻辑元件、数据库等。

2、物理架构

究竟是怎样做到在硬件当中放置软件元件。例如处于上海与北京进行分布的分布式系统的物理架构,这也就是说全部的元件都是属于物理设备,主要的有主机、整合服务器、应用服务器、代理服务器、存储服务器、报表服务器、Web服务器、网络分流器等。

你先找到标注了@SpringBootApplication的主类,注意看它的package。这个@SpringBootApplication会声明让Spring去扫描该package里以及所有子package里的类,如果扫到的类标注有@Component/@Controller/@Service/@Repository,那就把它加入Spring容器,这样你在其他任何地方使用@Autowired标注就能自动从Spring容器里把这个类找出来并注入进去直接使用。

package comdemo;

@SpringBootApplication

public class Application {

//

}

由于采用了 Access 数据库,数据库文件的绝对路径是获取数据库连接时的关键参数,而本例中又同时采用了 JSF 框架,JSF 页面调用 JavaBean 中的方法时与普通 JSP 页面不同,要想将 request 对象直接传递到 JavaBean 的相关方法中来获取站点路径是比较麻烦的。前后思索想到一个笨方法,可以直接在获取连接的方法中获得路径,无需从 JSF 页面获取参数。代码如下:

public static Connection getConnection() {

  // 获得当前 JSF 上下文环境

  FacesContext context = FacesContextgetCurrentInstance();

  // 获得 FacesContext 的 Application 对象

  Application application = contextgetApplication();

  // 获得 classes 目录的绝对路径

  URL classesUrl = applicationgetClass()getResource("/");

  // 获得数据库文件的绝对路径

  String dbpath = classesUrltoString() + "//data/blogDatamdb";

  // 截去 URL 前端文件访问协议 file:// 字符串

  dbpath = dbpathsubstring(6);

  String url = "jdbc:odbc:driver={Microsoft Access Driver (mdb)};DBQ="

      + dbpath;

  Connection conn = null;

  try {

    ClassforName("sunjdbcodbcJdbcOdbcDriver");

    conn = DriverManagergetConnection(url, "", "");

  }catch(Exception ex) {

    exprintStackTrace();

  }

  return conn;

}

你的spring配置有问题吧 很明显获取的对象和TeacherServiceImpl对象不一致,如果你是面向接口的编程,那就直接

(TeacherService)contextgetBean("teacherService")

TeacherService是 TeacherServiceImpl的接口,然后通过调用直接调用实现类的方法,关键是spring中teacherService要配置成 类TeacherServiceImpl

以上就是关于图文并茂,揭秘 Spring 的 Bean 的加载过程全部的内容,包括:图文并茂,揭秘 Spring 的 Bean 的加载过程、怎样得到bean中的所有字段、如何接收配置的bean的返回值等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/10129161.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-05
下一篇 2023-05-05

发表评论

登录后才能评论

评论列表(0条)

保存