一直在用的 Spring,你知道它的加载原理吗?

Java大联盟

共 14239字,需浏览 29分钟

 ·

2020-08-04 12:16

  Java大联盟

  帮助万千Java学习者持续成长

关注



作者|拥抱心中的梦想

juejin.im/post/5ab30714f265da237b21fbcc


B 站搜索:楠哥教你学Java

获取更多优质视频教程


一、前言

作为一个经常使用 Spring 的后端程序员,小编很早就想彻底弄懂整个 Spring 框架了!但它整体是非常大的,所有继承图非常复杂,加上小编修行尚浅,显得力不从心。不过,男儿在世当立志,今天就先从 Spring IOC 容器的初始化开始说起,即使完成不了对整个 Spring 框架的完全掌握,也不丢人,因为小编动手了,稳住,咱能赢!

下面说一些阅读前的建议:

1、阅读源码分析是非常无聊的,但既然你进来了,肯定也是对这个东西进行了解,也希望这篇总结能对你有所启发。

2、前方高能,文章可能会非常的长,图文并茂。

3、阅读前建议你对相关设计模式、软件设计 6 大原则有所了解,小编会在行文中进行穿插。

4、如果你发现文章观点有所错误或者与你见解有差异,欢迎指出和交流!

5、建议你边看文章的时候可以边在 IDE 中进行调试跟踪

6、文章所有 UML 图利用 idea 自动生成,具体生成方法为:选中一个类名,先ctrl+shift+alt+U,再ctrl+alt+B,然后回车即可


二、文章将围绕什么来进行展开?

不多,就一行代码,如下图:

这句是 Spring 初始化的代码,虽然只有一句代码,但内容贼多!


三、Spring 容器 IOC 有哪些东西组成?

这样子,小编先理清下思路,一步一步来:

1、上面那句代码有个文件叫applicationContext.xml, 这是个资源文件,由于我们的bean都在里边进行配置定义,那 Spring 总得对这个文件进行读取并解析吧!所以 Spring 中有个模块叫Resource模块,顾名思义,就是资源嘛!用于对所有资源xml、txt、property等文件资源的抽象。

下面先贴一张小编生成的类图(图片有点大,不知道会不会不清晰,如果不清晰可以按照上面说的idea生成方法去生成即可)


可以看到Resource是整个体系的根接口,点进源码可以看到它定义了许多的策略方法,因为它是用了策略模式这种设计模式,运用的好处就是策略接口/类定义了同一的策略,不同的子类有不同的具体策略实现,客户端调用时传入一个具体的实现对象比如UrlResource或者FileSystemResource给策略接口/类Resource即可!

所有策略如下:



2、上面讲了 Spring 框架对各种资源的抽象采用了策略模式,那么问题来了,现在表示资源的东西有了,那么是怎么把该资源加载进来呢?于是就有了下面的ResourceLoader组件,该组件负责对 Spring 资源的加载,资源指的是xml、properties等文件资源,返回一个对应类型的Resource对象。。UML 图如下:



从上面的 UML 图可以看出,ResourceLoader组件其实跟Resource组件差不多,都是一个根接口,对应有不同的子类实现,比如加载来自文件系统的资源,则可以使用FileSystemResourceLoader, 加载来自ServletContext上下文的资源,则可以使用ServletContextResourceLoader。还有最重要的一点。


从上图看出,ApplicationContext,AbstractApplication是实现了ResourceLoader的,这说明什么呢?说明我们的应用上下文ApplicationContext拥有加载资源的能力,这也说明了为什么可以通过传入一个String resource path给ClassPathXmlApplicationContext("applicationContext.xml")就能获得 xml 文件资源的原因了!清晰了吗?nice!


3、上面两点讲到了,好!既然我们拥有了加载器ResourceLoader,也拥有了对资源的描述Resource, 但是我们在 xml 文件中声明的标签在 Spring 又是怎么表示的呢?


注意这里只是说对bean的定义,而不是说如何将转换为bean对象。我想应该不难理解吧!就像你想表示一个学生Student,那么你在程序中肯定要声明一个类Student吧!


至于学生数据是从excel导入,或者程序运行时new出来,或者从xml中加载进来这些都不重要,重要的是你要有一个将现实中的实体表示为程序中的对象的东西,所以也需要在 Spring 中做一个定义!于是就引入一个叫BeanDefinition的组件,UML 图如下:



下面讲解下 UML 图:


首先配置文件中的标签跟我们的BeanDefinition是一一对应的,元素标签拥有class、scope、lazy-init等配置属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性。


4、有了加载器ResourceLoader,也拥有了对资源的描述Resource,也有了对bean的定义,我们不禁要问,我们的Resource资源是怎么转成我们的BeanDefinition的呢? 因此就引入了BeanDefinitionReader组件, Reader 嘛!就是一种读取机制,UML 图如下:



从上面可以看出,Spring 对 reader 进行了抽象,具体的功能交给其子类去实现,不同的实现对应不同的类,如PropertiedBeanDefinitionReader,XmlBeanDefinitionReader对应从 Property 和 xml 的 Resource 解析成BeanDefinition。


5、好了!基本上所有组件都快齐全了!对了,还有一个组件,你有了BeanDefinition后,你还必须将它们注册到工厂中去,所以当你使用getBean()方法时工厂才知道返回什么给你。


还有一个问题,既然要保存注册这些bean, 那肯定要有个数据结构充当容器吧!没错,就是一个Map, 下面贴出BeanDefinitionRegistry的一个实现,叫SimpleBeanDefinitionRegistry的源码图:



BeanDefinitionRegistry的 UML 图如下:



从图中可以看出,BeanDefinitionRegistry有三个默认实现,分别是SimpleBeanDefinitionRegistry,DefaultListableBeanFactory,GenericApplicationContext, 其中SimpleBeanDefinitionRegistry,DefaultListableBeanFactory都持有一个 Map。


也就是说这两个实现类把保存了 bean。而GenericApplicationContext则持有一个DefaultListableBeanFactory对象引用用于获取里边对应的 Map。在DefaultListableBeanFactory中



在GenericApplicationContext中



6、前面说的 5 个点基本上可以看出ApplicationContext上下文基本直接或间接贯穿所有的部分,因此我们一般称之为容器,除此之外,ApplicationContext还拥有除了bean容器这种角色外,还包括了获取整个程序运行的环境参数等信息(比如 JDK 版本,jre 等),其实这部分 Spring 也做了对应的封装,称之为Enviroment, 下面就跟着小编的 eclipse, 一起 debug 下容器的初始化工程吧!



四、实践是检验真理的唯一标准

学生类Student.java如下:

package com.wokao666;
public class Student {
private int id; private String name; private int age;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public Student(int id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; }
public Student() { super(); }
@Override public String toString() { return "Student [id=" + id + ", ]"; }
}

在application.xml中进行配置,两个bean:

<bean id="stu1" class="com.wokao666.Student">   <property >property>   <property >property>   <property >property> bean>  <bean id="stu2" class="com.wokao666.Student">   <property >property>   <property >property>   <property >property> bean>

好了,接下来给最开头那段代码打个断点 (Breakpoint):


第一步:急切地加载ContextClosedEvent类,以避免在WebLogic 8.1中的应用程序关闭时出现奇怪的类加载器问题。




这一步无需太过在意!


第二步:既然是new ClassPathXmlApplicationContext() 那么就调用构造器嘛!


第三步:


第四步:

好,我们跟着第三步中的super(parent),再结合上面第三节的第 6 小点 UML 图一步一步跟踪,然后我们来到AbstractApplicationContext的这个方法:


那么里边的resourcePatternResolver的类型是什么呢?属于第三节说的 6 大步骤的哪个部分呢?通过跟踪可以看到它的类型是ResourcePatternResolver类型的,而ResourcePatternResolver又是继承了ResourceLoader接口,因此属于加载资源模块,如果还不清晰,咱们再看看ResourcePatternResolver的源码即可,如下图:


对吧!不仅继承ResourceLoader接口,而且只定义一个getResources()方法用于返回Resource[]资源集合。再者,这个接口还使用了策略模式,其具体的实现都在实现类当中,好吧!来看看 UML 图就知道了!


PathMatchingResourcePatternResolver这个实现类呢!它就是用来解释不同路径资源的,比如你传入的资源路径有可能是一个常规的url, 又或者有可能是以classpath*前缀,都交给它处理。


ServletContextResourcePatternResolver这个实现类顾名思义就是用来加载Servlet上下文的,通常用在 web 中。


第五步:

接着第四步的方法,我们在未进入第四步的方法时,此时会对AbstractApplicationContext进行实例化,此时this对象的某些属性被初始化了(如日志对象),如下图:


接着进入getResourcePatternResolver()方法:


第四步说了,PathMatchingResourcePatternResolver用来处理不同的资源路径的,怎么处理,我们先进去看看!


如果找到,此时控制台会打印找到用于OSGi包URL解析的Equinox FileLocator日志。没打印很明显找不到!

运行完成返回setParent()方法。


第六步:


如果父代是非null,,则该父代与当前this应用上下文环境合并。显然这一步并没有做什么事!parent显然是null的,那么就不合并嘛!还是使用当前this的环境。

做个总结:前六步基本上做了两件事:

  • 1、初始化相关上下文环境,也就是初始化ClassPathXmlApplicationContext实例

  • 2、获得一个resourcePatternResolver对象,方便第七步的资源解析成Resource对象


第七步:



第七步又回到刚开始第三步的代码,因为我们前面 6 步已经完成对super(parent)的追踪。让我们看看setConfigLocation()方法是怎么一回事~

/** * Set the config locations for this application context.//未应用上下文设置资源路径 * 

If not set, the implementation may use a default as appropriate.//如果未设置,则实现可以根据需要使用默认值。 */public void setConfigLocations(String... locations) { if (locations != null) {//非空 Assert.noNullElements(locations, "Config locations must not be null");//断言保证locations的每个元素都不为null this.configLocations = new String[locations.length]; for (int i = 0; i < locations.length; i++) { this.configLocations[i] = resolvePath(locations[i]).trim();//去空格,很好奇resolvePath做了什么事情? } } else { this.configLocations = null; }}


进入resolvePath()方法看看:

/** * 解析给定的资源路径,必要时用相应的环境属性值替换占位符,应用于资源路径配置。 * Resolve the given path, replacing placeholders with corresponding * environment property values if necessary. Applied to config locations. * @param path the original file path * @return the resolved file path * @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) */protected String resolvePath(String path) {  return getEnvironment().resolveRequiredPlaceholders(path);}
进入getEnvironment()看看:/** * {@inheritDoc} *

If {@code null}, a new environment will be initialized via * {@link #createEnvironment()}. */@Overridepublic ConfigurableEnvironment getEnvironment() { if (this.environment == null) { this.environment = createEnvironment(); } return this.environment;}


进入createEnvironment(), 方法,我们看到在这里创建了一个新的StandardEnviroment对象,它是Environment的实现类,表示容器运行的环境,比如 JDK 环境,Servlet 环境,Spring 环境等等。


每个环境都有自己的配置数据,如System.getProperties()、System.getenv()等可以拿到 JDK 环境数据;ServletContext.getInitParameter()可以拿到 Servlet 环境配置数据等等, 也就是说 Spring 抽象了一个Environment来表示环境配置。


生成的StandardEnviroment对象并没有包含什么内容,只是一个标准的环境,所有的属性都是默认值。


第八步:这一步是重头戏

先做个小结:到现在为止,我们拥有了以下实例:


现在代码运行到如下图的refresh()方法:



看一下这个方法的内容是什么?

@Overridepublic void refresh() throws BeansException, IllegalStateException {  synchronized (this.startupShutdownMonitor) {    // 刷新前准备工作,包括设置启动时间,是否激活标识位,初始化属性源(property source)配置    prepareRefresh();
// 创建beanFactory(过程是根据xml为每个bean生成BeanDefinition并注册到生成的beanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//准备创建好的beanFactory(给beanFactory设置ClassLoader,设置SpEL表达式解析器,设置类型转化器【能将xml String类型转成相应对象】, //增加内置ApplicationContextAwareProcessor对象,忽略各种Aware对象,注册各种内置的对账对象【BeanFactory,ApplicationContext】等, //注册AOP相关的一些东西,注册环境相关的一些bean prepareBeanFactory(beanFactory);
try { // 模板方法,为容器某些子类扩展功能所用(工厂后处理器)这里可以参考BeanFactoryPostProcessor接口的postProcessBeanFactory方法 postProcessBeanFactory(beanFactory);
// 调用所有BeanFactoryPostProcessor注册为Bean invokeBeanFactoryPostProcessors(beanFactory);
// 注册所有实现了BeanPostProcessor接口的Bean registerBeanPostProcessors(beanFactory);
// 初始化MessageSource,和国际化相关 initMessageSource();
// 初始化容器事件传播器 initApplicationEventMulticaster();
// 调用容器子类某些特殊Bean的初始化,模板方法 onRefresh();
// 为事件传播器注册监听器 registerListeners();
// 初始化所有剩余的bean(普通bean) finishBeanFactoryInitialization(beanFactory);
// 初始化容器的生命周期事件处理器,并发布容器的生命周期事件 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // 销毁已创建的bean destroyBeans(); // 重置`active`标志 cancelRefresh(ex); throw ex; } finally { //重置一些缓存 resetCommonCaches(); } }}

在这里我想说一下,这个refresh()方法其实是一个模板方法, 很多方法都让不同的实现类去实现,但该类本身也实现了其中一些方法,并且这些已经实现的方法是不允许子类重写的,比如:prepareRefresh()方法。更多模板方法设计模式,可看我之前的文章 谈一谈我对‘模板方法’设计模式的理解(Template)。

先进入prepareRefresh()方法:


/** * Prepare this context for refreshing, setting its startup date and * active flag as well as performing any initialization of property sources. */protected void prepareRefresh() {  this.startupDate = System.currentTimeMillis();//设置容器启动时间  this.closed.set(false);//容器关闭标志,是否关闭?  this.active.set(true);//容器激活标志,是否激活?    if (logger.isInfoEnabled()) {//运行到这里,控制台就会打印当前容器的信息    logger.info("Refreshing " + this);  }
// 空方法,由子类覆盖实现,初始化容器上下文中的property文件 initPropertySources();
//验证标记为必需的所有属性均可解析,请参阅ConfigurablePropertyResolver#setRequiredProperties getEnvironment().validateRequiredProperties();
//允许收集早期的ApplicationEvents,一旦多播器可用,即可发布... this.earlyApplicationEvents = new LinkedHashSet();}

控制台输出:


三月 22, 2018 4:21:13 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@96532d6: startup date [Thu Mar 22 16:21:09 CST 2018]; root of context hierarchy

第九步:

进入obtainFreshBeanFactory()方法:

/** * 告诉子类刷新内部bean工厂(子类是指AbstractApplicationContext的子类,我们使用的是ClassPathXmlApplicationContext) * Tell the subclass to refresh the internal bean factory. */protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {  refreshBeanFactory();//刷新Bean工厂,如果已经存在Bean工厂,那就关闭并销毁,再创建一个新的bean工厂  ConfigurableListableBeanFactory beanFactory = getBeanFactory();//获取新创建的Bean工厂  if (logger.isDebugEnabled()) {    logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);//控制台打印  }  return beanFactory;}
进入refreshBeanFactory()方法:/** * 该实现执行该上下文的基础Bean工厂的实际刷新,关闭以前的Bean工厂(如果有的话)以及为该上下文的生命周期的下一阶段初始化新鲜的Bean工厂。 * This implementation performs an actual refresh of this context's underlying * bean factory, shutting down the previous bean factory (if any) and * initializing a fresh bean factory for the next phase of the context's lifecycle. */@Overrideprotected final void refreshBeanFactory() throws BeansException { if (hasBeanFactory()) {//如果已有bean工厂 destroyBeans();//销毁 closeBeanFactory();//关闭 } try { DefaultListableBeanFactory beanFactory = createBeanFactory();//创建一个新的bean工厂 beanFactory.setSerializationId(getId());//为序列化目的指定一个id,如果需要,可以将此BeanFactory从此id反序列化回BeanFactory对象。 //定制容器,设置启动参数(bean可覆盖、循环引用),开启注解自动装配 customizeBeanFactory(beanFactory); ////将所有BeanDefinition载入beanFactory中,此处依旧是模板方法,具体由子类实现 loadBeanDefinitions(beanFactory); //beanFactory同步赋值 synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); }}

总结:这一步主要的工作就是判断刷新容器前是否已经有 beanfactory 存在,如果有,那么就销毁旧的 beanfactory, 那么就销毁掉并且创建一个新的 beanfactory 返回给容器,同时将 xml 文件的BeanDefinition注册到 beanfactory 中。如果不太清楚可以回过头看看我们的第三节第5点内容


第十步:

进入第九步的loadBeanDefinitions(beanFactory)方法中去take a look:

/** * 使用XmlBeanDefinitionReader来加载beandefnition,之前说过使用reader机制加载Resource资源变为BeanDefinition对象 * Loads the bean definitions via an XmlBeanDefinitionReader. * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader * @see #initBeanDefinitionReader * @see #loadBeanDefinitions */@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {  // 创建XmlBeanDefinitionReader对象  XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 使用当前上下文Enviroment中的Resource配置beanDefinitionReader,因为beanDefinitionReader要将Resource解析成BeanDefinition嘛! beanDefinitionReader.setEnvironment(this.getEnvironment()); beanDefinitionReader.setResourceLoader(this); beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
//初始化这个reader initBeanDefinitionReader(beanDefinitionReader); //将beandefinition注册到工厂中(这一步就是将bean保存到Map中) loadBeanDefinitions(beanDefinitionReader);}

控制台输出:

三月 22, 2018 5:09:40 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions信息: Loading XML bean definitions from class path resource [applicationContext.xml]

第十一步:

进入prepareBeanFactory(beanFactory)方法:

//设置bean类加载器//设置Spring语言表达式(SpEL)解析器//扫描ApplicationContextAware bean//注册类加载期类型切面织入(AOP)LoadTimeWeaver//为各种加载进入beanFactory的bean配置默认环境

第十二步:

postProcessBeanFactory(beanFactory)方法:

postProcessBeanFactory同样作为一个模板方法,由子类来提供具体的实现,子类可以有自己的特殊对BeanDefinition后处理方法,即子类可以在这对前面生成的BeanDefinition,即bean的元数据再处理。比如修改某个bean的id/name属性、scope属性、lazy-init属性等。


第十三步:

invokeBeanFactoryPostProcessors(beanFactory)方法:

该方法调用所有的BeanFactoryPostProcessor,它是一个接口,实现了此接口的类需重写postProcessBeanFactory()这个方法,可以看出该方法跟第十二步的方法是一样的,只不过作为接口,更多的是提供给开发者来对生成的BeanDefinition做处理,由开发者提供处理逻辑。


第十四步:

其余剩下的方法基本都是像初始化消息处理源,初始化容器事件,注册bean监听器到事件传播器上,最后完成容器刷新。


五、总结

恭喜我,我终于写完了,同样也恭喜你,你也阅读完了。

我很佩服我自己能花这么长时间进行总结发布,之所以要进行总结,那是因为小编还是赞同好记性不如烂笔头的说法。

你不记,你过阵子就会忘记,你若记录,你过阵子也会忘记!区别在于忘记了,可以回过头在很短的时间内进行回忆,查漏补缺,减少学习成本。

再者,我认为我分析的还不是完美的,缺陷很多,因此我将我写的所有文章发布出来和大家探讨交流,汕头大学有校训说得非常地好,那就是说知识是用来共享的,因为共享了,知识才能承前启后。


现在再梳理一下 Spring 初始化过程:


1、首先初始化上下文,生成ClassPathXmlApplicationContext对象,在获取resourcePatternResolver对象将xml解析成Resource对象。


2、利用 1 生成的 context、resource 初始化工厂,并将 resource 解析成 beandefinition, 再将 beandefinition 注册到 beanfactory 中。


推荐阅读

1、Spring Boot+Vue项目实战

2、B站:4小时上手MyBatis Plus

3、一文搞懂前后端分离

4、快速上手Spring Boot+Vue前后端分离


楠哥简介

资深 Java 工程师,微信号 southwindss

《Java零基础实战》一书作者

腾讯课程官方 Java 面试官今日头条认证大V

GitChat认证作者,B站认证UP主(楠哥教你学Java)

致力于帮助万千 Java 学习者持续成长。




有收获,就在看 
浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报