0
点赞
收藏
分享

微信扫一扫

Spring源码系列:容器的基本实现

Resin_Wu 2022-02-26 阅读 49

Spring源码系列:容器的基本实现

前言

什么是容器?

  1. Spring容器Application的一个实例对象。
  2. 容器负责的实例化、配置Bean、管理Bean的生命周期。
  3. Spring容器将我们代码中的PoJo类、XML配置文件转化为一个可用的系统。

那么接下来会以spring-beans包下的一些核心类展开来讲解容器是怎么实现的。

一. 容器的基本实现

要想了解Spring容器的概念和深入源码,相信一切都得从这个容器的出生开始,那么在此以DefaultListableBeanFactory为切入点来讲解。

那么DefaultListableBeanFactory类是干什么的呢?用百度翻译一下源码中的注释,如下:

  1. 作为ConfigurableListableBeanFactoryBeanDefinitionRegistry接口的默认实现。
  2. 一个基于bean定义元数据的工厂类,用于注册所有的bean。(可能是PoJo类、配置文件)。
  3. 可以操作预先解析的bean元数据对象。

那么来看下这个类的关系图:
在这里插入图片描述
我们可以重点关注图中蓝色框圈起来的部分,我们可以做个总结,DefaultListableBeanFactory类对bean的作用有两个方向:

  • 进行监听,对于满足条件的bean进行定义注册
  • bean进行增删改查操作(一些动作实现)

DefaultListableBeanFactory作为整个bean加载的核心部分,是Spring注册和加载bean的一个默认实现。其还有个子类XmlBeanFactory,主要用于从XML文件中读取BeanDefinition

我个人理解是这样的:

  • 一些PoJo类,其加载一般交给DefaultListableBeanFactory来执行。
  • 而对于XML形式配置的Bean,则交给XmlBeanFactory来执行。

Tip:

1.1 资源的读取

Spring的大部分功能都是以配置作为切入点。而上文提到的,XmlBeanFactory负责XML配置形式的Bean的加载。而从XML这类资源文件读取、解析以及注册等流程,则交给XmlBeanDefinitionReader来完成。

来看下XmlBeanDefinitionReader的类关系图:
在这里插入图片描述
XmlBeanDefinitionReader类下有这么几个重要的成员:

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
	// 定义从资源文件加载到转化为Document的功能
	private DocumentLoader documentLoader = new DefaultDocumentLoader();
	// 前者负责转化为Document,那么documentReaderClass 就负责读取Document 并注册 BeanDefinition
	private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
}

XmlBeanDefinitionReader类的父类AbstractBeanDefinitionReader下又有这么几个重要的成员:

public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
	// 定义资源加载器,主要应用于根据给定的资源文件地址,返回对应的Resource。
	private ResourceLoader resourceLoader;
}

将上文做个总结,XmlBeanDefinitionReader主要做的事情就是:

  1. 利用父类AbstractBeanDefinitionReaderResourceLoader将资源文件路径转化为对应的Resource
  2. Resource进行文件转换,转换为Document文件。
  3. 使用DefaultBeanDefinitionDocumentReader进行文件解析。

1.1.1 Resource资源

Spring的配置文件是通过ClassPathResource来封装的,我们来看下他的类关系图:
在这里插入图片描述
顶层接口InputStreamSource只提供了一个方法:提供返回InputStream流的方法。

public interface InputStreamSource {
	InputStream getInputStream() throws IOException;
}

Resource接口用于封装底层资源,抽象了所有Spring内部使用到的资源:FlieURLClasspath等。提供了3个判断当前资源状态的方法。

  1. 存在性exists()
  2. 可读性isReadable()
  3. 是否处于打开状态isOpen()

同时还提供了不同资源到URLURIFile类型的转换,Resource接口的具体实现有:

  1. FileSystemResource(文件)。
  2. ClassPathResourceClassPath资源)。
  3. UrlResourceURL资源)。
  4. InputStreamSourceInputStream资源)。
  5. ByteArrayResourceByte数组)。

1.2 资源的加载和解析

在Spring将配置文件封装为Resource类型的实例后,就会由XmlBeanDefinitionReader来完成资源加载。
我们来直接看其核心方法loadBeanDefinitions(),上文读取好的资源文件(Resource实例)则作为其参数传入:

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
	@Override
	public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(new EncodedResource(resource));
	}
}

直观的来看这个方法,我们发现,在做Bean加载之前,会对Resource实例对象进行编码。

// 先对Resource资源进行编码封装
new EncodedResource(resource)

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
	Assert.notNull(encodedResource, "EncodedResource must not be null");
	if (logger.isTraceEnabled()) {
		logger.trace("Loading XML bean definitions from " + encodedResource);
	}
	// 用来记录 已经加载完成的资源 的Set集合
	Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
	// 1.如果发现,该资源已经被加载过,那么抛异常,说明你这个资源重复加载了。
	if (!currentResources.add(encodedResource)) {
		throw new BeanDefinitionStoreException(
				"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
	}
	// 2.获取每个Resource资源对应的inputStream流
	try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
		InputSource inputSource = new InputSource(inputStream);
		// 2.1 设置对应的编码,这是考虑到Resource可能存在编码要求的情况
		if (encodedResource.getEncoding() != null) {
			inputSource.setEncoding(encodedResource.getEncoding());
		}
		// 2.2 进行真正的Bean加载
		return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
	}
	// ...
}

再看下核心的doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {

	try {
		Document doc = doLoadDocument(inputSource, resource);
		int count = registerBeanDefinitions(doc, resource);
		if (logger.isDebugEnabled()) {
			logger.debug("Loaded " + count + " bean definitions from " + resource);
		}
		return count;
	}
	// 以下都是各种catch方法,我们主要关注try语句块中做的事情即可。
}

这个方法主要做三件事情:

  1. 获取XML文件的验证模式。
  2. 加载XML文件,并得到对应的Document
  3. 根据返回的Document注册Bean信息。

doLoadDocument()这个方法,则做了前两件事情。我们以这行代码为切入点,来展开。

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
	// 这里的documentLoader指的是上文提到的DefaultDocumentLoader,负责将Resource实例转化为Document
	return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
			getValidationModeForResource(resource), isNamespaceAware());
}

1.2.1 获取XML的验证模式

XML的验证模式有什么用?其保证了XML文件的正确性。常用的验证模式有两种:

DTD

Document Type Definition:文档类型定义,一种XML约束模式语言,是XML文件的验证机制。 属于XML文件组成的一部分。可以通过比较XML文档和DTD文件来判断文档是否符合规范。一个DTD文档包含:

  1. 元素的定义规则。
  2. 元素间关系的定义规则。
  3. 元素可使用的属性。
  4. 可使用的实体或者符号规则。

DTD案例:注意DOCTYPE

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"  
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">  
<beans>  
  
</beans>  

XSD

XML Schemas Definition:XML Schema语言就是XSD。描述了XML文档的结构。XML Schema 本身就是XML文档,符合其语法结构,可以用通用的XML解析器来解析。一个XSD包括:

  1. 文档中出现的元素。
  2. 文档中出现的属性、子元素。
  3. 子元素的数量和顺序。
  4. 元素是否为空。
  5. 元素和属性的数据类型。
  6. 元素或属性的默认和固定值。

XSD案例:

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"  
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="  
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd  
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd  
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">  
  
</beans>  

使用XML文档的时候,必须做到几点:

  1. 声明名称空间xmlns="http://www.springframework.org/schema/beans"
  2. 指定该名称空间对应的XML Schema文档的存储位置xsi:schemaLocation="xxx"一部分是名称空间的URI,另一部分是该名称空间所标识的XML Schema文件位置或者URL地址。

其他:

  1. 声明XML Schema 实例xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

言归正传,我们回到代码本身,来关注下getValidationModeForResource()方法:

protected int getValidationModeForResource(Resource resource) {
	int validationModeToUse = getValidationMode();
	// 如果手动制定了验证模式,则使用指定的验证模式
	if (validationModeToUse != VALIDATION_AUTO) {
		return validationModeToUse;
	}
	// 如果没有手动指定,那么使用自动检测
	int detectedMode = detectValidationMode(resource);
	if (detectedMode != VALIDATION_AUTO) {
		return detectedMode;
	}
	return VALIDATION_XSD;
}

其实detectValidationMode()方法并不是很难理解,我只会贴出最最核心的代码:

private static final String DOCTYPE = "DOCTYPE";

if (hasDoctype(content)) {
	isDtdValidated = true;
	break;
}

private boolean hasDoctype(String content) {
	return content.contains(DOCTYPE);
}

说白了就是,如果发现文档中包含了DOCTYPE,该XML模式就是DTD,否则就是XSD。(看到这里可以回顾下上文的DTD案例)

1.2.2 获取Document

在验证完XML模式的合法性后,会将Resource实例转化为Document,再来回顾这行代码:

protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
	// 这里的documentLoader指的是上文提到的DefaultDocumentLoader,负责将Resource实例转化为Document
	return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
			getValidationModeForResource(resource), isNamespaceAware());
}

其本质是通过SAX解析XML文档。

我们先来看下getEntityResolver()这个方法是干什么的:

protected EntityResolver getEntityResolver() {
	if (this.entityResolver == null) {
		// Determine default EntityResolver to use.
		ResourceLoader resourceLoader = getResourceLoader();
		if (resourceLoader != null) {
			this.entityResolver = new ResourceEntityResolver(resourceLoader);
		}
		else {
			this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
		}
	}
	return this.entityResolver;
}

EntityResolver

什么是EntityResolver

而对于项目本身而言,则可以提供一个寻找DTD声明的方法。
我们来看下EntityResolver接口:

public interface EntityResolver {
    public abstract InputSource resolveEntity (String publicId, String systemId) throws SAXException, IOException;
}

他接收俩参数,publiIdsystemId
以上文的DTDXSD案例为例,若是读取XSD配置文件,则获得的参数如下:

  • publiIdnull
  • systemIdhttp://www.springframework.org/schema/beans/spring-beans-3.0.xsd

若读取的是DTD文件:

  • publiId-//SPRING//DTD BEAN 2.0//EN
  • systemIdhttp://www.springframework.org/dtd/spring-beans-2.0.dtd

为啥会出现不同呢?Spring使用DelegatingEntityResolver来实现该接口:

public class DelegatingEntityResolver implements EntityResolver {
	public static final String DTD_SUFFIX = ".dtd";
	public static final String XSD_SUFFIX = ".xsd";
	@Override
	@Nullable
	public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException {
		if (systemId != null) {
			// 若加载dtd类型,则直接截取systemId最后的xx.dtd,然后去当前路径下寻找
			if (systemId.endsWith(DTD_SUFFIX)) {
				return this.dtdResolver.resolveEntity(publicId, systemId);
			}
			// 若加载xsd类型,则默认到META-INF/Spring.schemas文件中找到systemId对应的XSD文件并加载。
			else if (systemId.endsWith(XSD_SUFFIX)) {
				return this.schemaResolver.resolveEntity(publicId, systemId);
			}
		}
		return null;
	}
}

最后,关于如何转换Document,即loadDocument()方法的最终实现就简单概括,其由DefaultDocumentLoader来完成。

public class DefaultDocumentLoader implements DocumentLoader {
	@Override
	public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

		DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
		if (logger.isTraceEnabled()) {
			logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
		}
		DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
		return builder.parse(inputSource);
	}
}

主要做三件事:

  1. 创建DocumentBuilderFactory工厂。
  2. 工厂创建一个文档构造器DocumentBuilder
  3. 解析inputSource来生成Document对象。

1.2.3 解析和注册BeanDefinition

上文的代码里,只剩下这行代码没有讲解了,也就是在将文件转化为Document后,重点做的事情:提取和注册Bean

// doc则是1.2.2中获取到的Document
int count = registerBeanDefinitions(doc, resource);

代码展开:

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
		// 1.实例化BeanDefinitionDocumentReader 
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		// 2.获取之前已经加载好的BeanDefinition个数
		int countBefore = getRegistry().getBeanDefinitionCount();
		// 3.加载和注册Bean
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		// 4.记录本次加载的BeanDefinition个数
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}
}

BeanDefinitionDocumentReader只是一个接口,应用单一职责的原则,将具体的逻辑registerBeanDefinitions()方法委托给单一的类去进行处理。具体的实现类为DefaultBeanDefinitionDocumentReader,我们来看下其具体的实现:

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
	@Override
	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
		doRegisterBeanDefinitions(doc.getDocumentElement());
	}
	
	protected void doRegisterBeanDefinitions(Element root) {
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

		if (this.delegate.isDefaultNamespace(root)) {
			// 处理profile属性
			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)) {
					if (logger.isDebugEnabled()) {
						log..
					}
					return;
				}
			}
		}
		// 解析前处理,交给子类实现。父类给子类提供模板,即模板模式的一个体现
		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
		// 解析后处理,交给子类实现
		postProcessXml(root);

		this.delegate = parent;
	}
}

profile属性,用于在配置文件中指定开发环境,这样可以方便的进行切换开发、部署环境。常用的是更换不同的数据库。
如同一个配置文件中:

<bean profile="dev">xx</bean>
<bean profile="production">xx</bean>

那么集成到Web环境中,则在web.xml中加入以下代码:

<context-param>
	<param-name>Spring.profiles.active</param-name>
	<param-value>dev</param-value>
</context-param>

接下来再看看parseBeanDefinitions方法:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
	// bean处理
	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 ele) {
				// bean处理
				if (delegate.isDefaultNamespace(ele)) {
					parseDefaultElement(ele, delegate);
				}else {
					// bean处理
					delegate.parseCustomElement(ele);
				}
			}
		}
	}
	else {
		delegate.parseCustomElement(root);
	}
}

Spring的XML配置中Bean的声明方式有两大类:

  • 默认的:<bean id = "xx" class="xx.xx"/>
  • 自定义的:<tx:annotation-driven/>

上述代码也就是对不同情况进行不同的Bean处理。其中核心的parseDefaultElementparseCustomElement方法则在下文继续讲解。

1.3 总结

在这里,先对上文做个总结,方便大家思考和理解。

DefaultListableBeanFactory作为Bean加载的一个核心部分,是Spring注册和加载Bean的一个默认实现。有两个重要的功能:

  • 注册和加载Bean。(顶层实现AliasRegistry
  • Bean进行增删改查等操作。(顶层实现BeanFactory)

Spring的大部分功能都是以配置作为切入点。XML这类资源文件的读取、解析注册都是在XmlBeanDefinitionReader类中来完成。

资源读取:

  • Spring有自己的资源接口Resource,用于将不同类型的资源对象抽象成Resource实例对象。

资源解析:

  • XmlBeanDefinitionReaderloadBeanDefinitions()方法进行Bean的加载解析。
  • Resource实例对象进行编码。
  • 获取XML文件的验证模式(共两种:DTDXSD)。
  • 若验证通过,加载XML文件(使用Sax解析,即一边扫描XML一边解析),将Resource实例对象中的InputStream流转化为对应的Document对象doc

资源注册:

  • 根据doc来提取和注册Bean
  • 通过DefaultBeanDefinitionDocumentReaderregisterBeanDefinitions()方法先处理profile属性(用于在配置文件中指定开发环境)。
  • 然后再解析标签处理生成BeanDefinition

到这里Spring的Bean容器(工厂)对资源的处理工作也就做完了,更深层次的,对于Bean层面的解析和加载则交给后文。

二. 简单的小案例

我是在Spring5.0.x版本源码项目上,创建了自己的Test,如图:
在这里插入图片描述
创建User类:

public class User {
	private int id;
	private String name;
	// get set
}

test目录下的resources资源文件目录中,创建user.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	   xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
	   xsi:schemaLocation="
   	http://www.springframework.org/schema/beans
   	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   	http://www.springframework.org/schema/aop
   	http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
   	http://www.springframework.org/schema/tx
   	http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
   	http://www.springframework.org/schema/context
   	http://www.springframework.org/schema/context/spring-context-3.0.xsd">
	<bean name="user" class="org.springframework.beans.User">
		<property name="id" value="1" />
		<property name="name" value="你好" />
	</bean>
</beans>

测试类:

package org.springframework.beans;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class Test {
	@org.junit.jupiter.api.Test
	public void test() {
		ClassPathResource resource = new ClassPathResource("user.xml");
		XmlBeanFactory factory = new XmlBeanFactory(resource);
		User user = (User) factory.getBean("user");
		System.out.println(user.getId());
		System.out.println(user.getName());
	}
}

结果如下:
在这里插入图片描述
一般读取XML形式的Bean,有三步:

  1. 通过ClassPathResource加载对应的xml文件。
  2. 通过resource实例对象创建出Bean工厂XmlBeanFactory
  3. Bean工厂通过Name来获取对应的Bean

备注:注意,XmlBeanFactory对于Spring来说,已经是个过时的类了。不推荐使用。上述代码可以改为(本质一样的):

@org.junit.jupiter.api.Test
public void test() {
	BeanFactory factory = new DefaultListableBeanFactory();
	XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
	
	ClassPathResource resource = new ClassPathResource("user.xml");
	reader.loadBeanDefinitions(resource);
	
	User user = (User) factory.getBean("user");
	System.out.println(user.getId());
	System.out.println(user.getName());
}

本篇文章,从外层看,已经介绍了Bean容器对资源的一个加载和解析流程,而上述的案例中,Spring是如何把XML配置中的Bean加载进来的?又是如何得到我们配置的字段值的?答案也就是源码中核心的parseDefaultElementparseCustomElement方法。下篇文章则从标签的解析来做具体的展开介绍。

举报

相关推荐

0 条评论