共计 12062 个字符,预计需要花费 31 分钟才能阅读完成。
读完这篇文章你将会收获到
- 了解到 Spring 容器初始化流程
- ThreadLocal 在 Spring 中的最佳实践
- 面试中回答 Spring 容器初始化流程
引言
我们先从一个简单常见的代码入手分析
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd"> | |
<beans> | |
<bean class="com.demo.data.Person"> | |
<description> | |
微信搜一搜:CoderLi(不妨关注➕一下? 这次一定?) | |
</description> | |
</bean> | |
</beans> |
public static void main(String[] args) {Resource resource = new ClassPathResource("coderLi.xml"); | |
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory(); | |
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory); | |
xmlBeanDefinitionReader.loadBeanDefinitions(resource); | |
} |
上面这段 Java 代码主要做了
- 资源的获取(定位)
- 创建一个 beanFactory
- 根据 beanFactory (实现了 BeanDefinitionRegistry 接口) 创建一个 beanDefinitionReader
- 装载资源并 registry 资源里面的 beanDefinition
所以总体而言就是资源的加载、加载、注册三个步骤
- 对于资源的加载可以看看我另一篇文章 Spring- 资源加载(源码分析)
- 加载的过程则是将 Resource 对象转为一系列的 BeanDefinition 对象
- 注册则是将 BeanDefinition 注入到 BeanDefinitionRegistry 中
组件介绍
在分析源码流程之前我们一起先对一些重要的组件混个眼熟
DefaultListableBeanFactory
defaultListableBeanFactory 是整个 bean 加载的核心部分,是 bean 注册及加载 bean 的默认实现
对于 AliasRegistry 可以参考我另一篇文章 Spring-AliasRegistry。关于这个类我们只要记住两点,一个是它是一个 beanFactory、一个是它是一个 BeanDefinitionRegistry
XmlBeanDefinitionReader
从 XML 资源文件中读取并转换为 BeanDefinition 的各个功能
DocumentLoader
对 Resource 文件进行转换、将 Resource 文件转换为 Document 文件
BeanDefinitionDocumentReader
读取 Document 并向 BeanDefinitionRegistry 注册
源码分析
loadBeanDefinitions(Resource)
XmlBeanDefinitionReader#loadBeanDefinitions(Resource)
我们先从这个入口方法开始进去
@Override | |
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {return loadBeanDefinitions(new EncodedResource(resource)); | |
} |
EncodedResource
是 Resource 的子类,Spring- 资源加载(源码分析)
public class EncodedResource implements InputStreamSource { | |
private final Resource resource; | |
@Nullable | |
private final String encoding; | |
@Nullable | |
private final Charset charset; | |
.......... | |
.......... | |
public Reader getReader() throws IOException {if (this.charset != null) {return new InputStreamReader(this.resource.getInputStream(), this.charset); | |
} | |
else if (this.encoding != null) {return new InputStreamReader(this.resource.getInputStream(), this.encoding); | |
} | |
else {return new InputStreamReader(this.resource.getInputStream()); | |
} | |
} | |
} |
只是一个简单的 Wrapper 类,针对不同的字符集和字符编码返回不一样的 Reader
loadBeanDefinitions(EncodedResource)
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { | |
// 从 Thread Local 中获取正在加载的的资源 | |
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); | |
// 判断这个资源是否已经加载过了、主要是为了是否是 资源的循环依赖 import | |
if (!currentResources.add(encodedResource)) {throw new BeanDefinitionStoreException(""); | |
} | |
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {InputSource inputSource = new InputSource(inputStream); | |
// 有 encode 就设置进去 | |
if (encodedResource.getEncoding() != null) {inputSource.setEncoding(encodedResource.getEncoding()); | |
} | |
// 真正的加载 | |
return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); | |
} | |
catch (IOException ex) {throw new BeanDefinitionStoreException(""); | |
} | |
finally { | |
// ThreadLocal 的最佳实践 | |
currentResources.remove(encodedResource); | |
if (currentResources.isEmpty()) {this.resourcesCurrentlyBeingLoaded.remove(); | |
} | |
} | |
} |
首先从 ThreadLocal
中获取正在加载的 Resource
,这里主要是检查 import
标签带来的循环引用问题。
从这里我们可以看到在 finally
中,对已经完成加载的资源进行移除,并且检查 Set
是否还有元素了,如果没有则直接调用 ThreadLocal
的 remove
方法。这个就是 ThreadLocal 的最佳实践了,最后的 remove
方法的调用可以避免 ThreadLocal 在 ThreadLocalMap 中作为 WeakReference
而带来的内存泄露问题。
这个方法里基本做啥事情、最主要的事情就是调用了 doLoadBeanDefinitions
这个方法,而这个方法才是真正干活的。(在 Spring 中,很有意思的是、真正干活的方法前缀都是带有 do
的,这个可以留意下)
doLoadBeanDefinitions(InputSource Resource)
// 获取 document 对象 | |
Document doc = doLoadDocument(inputSource, resource); | |
// 注册 bean definition | |
int count = registerBeanDefinitions(doc, resource); | |
return count; |
doLoadDocument
这个方法就是将 Resource 转化为 Document,这里涉及到 xml 文件到验证,建立对应的 Document Node,使用到的就是上面提及到的 DocumentLoader
。这个不展开来探讨。
我们直接进入到 registerBeanDefinitions
方法中
registerBeanDefinitions(Document,Resource)
public int registerBeanDefinitions(Document doc, Resource resource) { | |
// 创建一个 bean definition 的 reader | |
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); | |
// 注册之前已经有的 bean definition 的个数 return this.beanDefinitionMap.size(); | |
int countBefore = getRegistry().getBeanDefinitionCount(); | |
documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); | |
return getRegistry().getBeanDefinitionCount() - countBefore; | |
} |
上面代码中出现了一个我们提及到的 BeanDefinitionDocumentReader
组件,他的功能就是读取 Document 并向 BeanDefinitionRegistry 注册
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { | |
this.readerContext = readerContext; | |
doRegisterBeanDefinitions(doc.getDocumentElement()); | |
} |
这里又来了、do
才是真正干活的大哥
protected void doRegisterBeanDefinitions(Element root) { | |
BeanDefinitionParserDelegate parent = this.delegate; | |
this.delegate = createDelegate(getReaderContext(), root, parent); | |
if (this.delegate.isDefaultNamespace(root)) { | |
// 处理 profiles | |
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); | |
if (StringUtils.hasText(profileSpec)) {String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); | |
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {return;} | |
} | |
} | |
// 解释前的处理 这里默认为空实现、子类可以覆盖此方法在解释 Element 之前做些事情 | |
preProcessXml(root); | |
// 解释 | |
parseBeanDefinitions(root, this.delegate); | |
// 解释后处理 这里默认为空实现 | |
postProcessXml(root); | |
this.delegate = parent; | |
} |
这里主要的方法就是 parseBeanDefinitions
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {if (delegate.isDefaultNamespace(root)) {NodeList nl = root.getChildNodes(); | |
for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i); | |
if (node instanceof Element) {Element ele = (Element) node; | |
if (delegate.isDefaultNamespace(ele)) { | |
// spring 默认标签解释 | |
parseDefaultElement(ele, delegate); | |
} else { | |
// 自定义 标签解释 | |
delegate.parseCustomElement(ele); | |
} | |
} | |
} | |
} else {delegate.parseCustomElement(root); | |
} | |
} |
Spring 的默认标签有 import
, beans
, bean
, alias
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { | |
// 解释 import 标签 | |
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {importBeanDefinitionResource(ele); | |
} else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {processAliasRegistration(ele); | |
} else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {processBeanDefinition(ele, delegate); | |
} else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {doRegisterBeanDefinitions(ele); | |
} | |
} |
解释 import
标签调用 importBeanDefinitionResource
最终会调用到我们最开始处理 Resource
循环依赖的那个方法 loadBeanDefinitions
中
我们直接进入到 processAliasRegistration
方法中
protected void processAliasRegistration(Element ele) {String name = ele.getAttribute(NAME_ATTRIBUTE); | |
String alias = ele.getAttribute(ALIAS_ATTRIBUTE); | |
boolean valid = true; | |
if (!StringUtils.hasText(name)) {getReaderContext().error("Name must not be empty", ele); | |
valid = false; | |
} | |
if (!StringUtils.hasText(alias)) {getReaderContext().error("Alias must not be empty", ele); | |
valid = false; | |
} | |
if (valid) { | |
try { | |
// 最重要的一行代码 | |
getReaderContext().getRegistry().registerAlias(name, alias); | |
} catch (Exception ex) {getReaderContext().error("Failed to register alias'" + alias + | |
"'for bean with name'" + name + "'", ele, ex); | |
} | |
getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); | |
} | |
} |
最重要的一行代码就是将 name 和 alias 进行注册 ( 这里注册的是 alias 标签中的 name 和 alias 之间的关系),可以参考这篇文章进行了解 Spring-AliasRegistry
我们来到最主要的 processBeanDefinition
protected void processBeanDefinition(Element ele, | |
BeanDefinitionParserDelegate delegate) { | |
// 这里获得了一个 BeanDefinitionHolder | |
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); | |
if (bdHolder != null) {bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); | |
try {BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); | |
} catch (BeanDefinitionStoreException ex) {.....} | |
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); | |
} | |
} |
我们先分析 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())
这句代码
public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) | |
throws BeanDefinitionStoreException { | |
// 注册 bean Name | |
String beanName = definitionHolder.getBeanName(); | |
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); | |
// 注册 alias . | |
String[] aliases = definitionHolder.getAliases(); | |
if (aliases != null) {for (String alias : aliases) {registry.registerAlias(beanName, alias); | |
} | |
} | |
} |
这个方法的作用很简单、就是使用一开始我们传给 XmlBeanDefinitionReader
的 BeanDefinitionRegistry
对 bean 和 beanDefinition 的关系进行注册。并且也对 beanName 和 alias 的关系进行注册 ( 这里是对 bean 标签中配置的 id 和 name 属性关系进行配置)
delegate.parseBeanDefinitionElement(ele)
我们再把眼光返回到这个方法、这个方法就是创建 BeanDefinition 的地方了
@Nullable | |
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {return parseBeanDefinitionElement(ele, null); | |
} |
@Nullable | |
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {String id = ele.getAttribute(ID_ATTRIBUTE); | |
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); | |
List<String> aliases = new ArrayList<>(); | |
// 判断是否配置了 name 属性、对 name 进行分割 | |
// 在 bean 标签中 name 就是 alias 了 | |
if (StringUtils.hasLength(nameAttr)) {String[] nameArr = StringUtils.tokenizeToStringArray(...); | |
aliases.addAll(Arrays.asList(nameArr)); | |
} | |
String beanName = id; | |
// 没有配置 id 并且 alias 列表不为空、则选取第一个 alias 为 bean Name | |
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {beanName = aliases.remove(0); | |
} | |
if (containingBean == null) { | |
// 检查 beanName 和 alias 的唯一性 | |
checkNameUniqueness(beanName, aliases, ele); | |
} | |
// 怎么生成一个 BeanDefinition 尼 | |
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); | |
if (beanDefinition != null) { | |
// 如果 beanName 为 空 | |
if (!StringUtils.hasText(beanName)) { | |
try {if (containingBean != null) { | |
beanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, this.readerContext.getRegistry(), true); | |
} else { | |
// 没有配置 beanName 和 alias 的话、那么这个类的第一个实例、将拥有 全类名的 alias | |
// org.springframework.beans.testfixture.beans.TestBean 这个是别名(TestBean#0 才拥有这个别名、其他的不配拥有) | |
// org.springframework.beans.testfixture.beans.TestBean#0 这个是 beanName | |
beanName = this.readerContext.generateBeanName(beanDefinition); | |
String beanClassName = beanDefinition.getBeanClassName(); | |
if (beanClassName != null && | |
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && | |
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {aliases.add(beanClassName); | |
} | |
} | |
} catch (Exception ex) {.........} | |
} | |
String[] aliasesArray = StringUtils.toStringArray(aliases); | |
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); | |
} | |
// nothing | |
return null; | |
} |
在 bean 标签中 name 属性对应的就是 alias,id 属性对应的就是 beanName 了
当我们没有配置 id 属性但是配置了 name 属性、那么第一个 name 属性就会成为我们的 id
当我们既没有配置 id 属性 也没有配置 name 属性,那么 Spring 就会帮我们生成具体可看看 Spring-AliasRegistry
然后就创建了一个 BeanDefinitionHolder 返回了
上面的代码我们看到有这个关键的方法 parseBeanDefinitionElement(ele, beanName, containingBean)
这个方法生成了我们期待的 BeanDefinition,但是里面的内容都是比较枯燥的
// 解释 class 属性 | |
String className = null; | |
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {className = ele.getAttribute(CLASS_ATTRIBUTE).trim();} | |
// 是否指定了 parent bean | |
String parent = null; | |
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {parent = ele.getAttribute(PARENT_ATTRIBUTE); | |
} | |
// 创建 GenericBeanDefinition | |
AbstractBeanDefinition bd = createBeanDefinition(className, parent); | |
// 解释各种默认的属性 | |
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); | |
// 提取 describe | |
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); | |
// 解释元数据 | |
parseMetaElements(ele, bd); | |
// look up 方法 | |
parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); | |
// replacer | |
parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); | |
// 解析构造函数参数 | |
parseConstructorArgElements(ele, bd); | |
// 解释 property 子元素 | |
parsePropertyElements(ele, bd); | |
// 解释 qualifier | |
parseQualifierElements(ele, bd); | |
bd.setResource(this.readerContext.getResource()); | |
bd.setSource(extractSource(ele)); |
都是去解析 bean 标签里面的各种属性
那么我们整个 Spring 容器初始化流程就介绍完了
总结
public static void main(String[] args) {Resource resource = new ClassPathResource("coderLi.xml"); | |
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory(); | |
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory); | |
xmlBeanDefinitionReader.loadBeanDefinitions(resource); | |
} |
- 调用 XmlBeanDefinitionReader 的方法 loadBeanDefinitions
- 将 Resource 包裹成 EncodeResource
- 通过 ThreadLocal 判断是否 Resource 循环依赖
- 使用 DocumentLoader 将 Resource 转换为 Document
- 使用 BeanDefinitionDocumentReader 解释 Document 的标签
-
解释 Spring 提供的默认标签 / 自定义的标签解释
- 解释 import 标签的时候会回调到步骤 2 中
- 解释 alias 标签会向 AliasRegistry 注册
- 解释 bean 标签会向 BeanDefinitionRegistry 注册 beanName 和 BeanDefinition,也会注册 bean 标签里面 id 和 name 的关系(其实就是 alias)
大致的流程就是如此了,面试的时候大致说出 XmlBeanDefinitionReader,DocumentLoader,BeanDefinitionDocumentReader,BeanDefinitionRegistry,AliasRegistry
这几个组件,面试官大概率会认为你是真的看过 Spring 的这部分代码的
往期相关文章
Spring- 资源加载(源码分析)
Spring-AliasRegistry
编译 Spring5.2.0 源码