栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

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

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

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

前言一. 容器的基本实现

1.1 资源的读取

1.1.1 Resource资源 1.2 资源的加载和解析

1.2.1 获取XML的验证模式

DTDXSD 1.2.2 获取document

EntityResolver 1.2.3 解析和注册BeanDefinition 1.3 总结 二. 简单的小案例

前言

什么是容器?

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

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

一. 容器的基本实现

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

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

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

那么来看下这个类的关系图:

我们可以重点关注图中蓝色框圈起来的部分,我们可以做个总结,DefaultListableBeanFactory类对bean的作用有两个方向:

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

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

我个人理解是这样的:

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

Tip:

Definition是什么意思?其单词本意是:定义。
而BeanDefinition像是对Bean的一个抽象模板,定义了Bean的一些行为、属性等。
那么自然而然的,BeanDefinitionRegistry就是该模板的注册器了。

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 documentReaderClass = DefaultBeanDefinitiondocumentReader.class;
}

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

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

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

    利用父类AbstractBeanDefinitionReader的ResourceLoader来将资源文件路径转化为对应的Resource。将Resource进行文件转换,转换为document文件。使用DefaultBeanDefinitiondocumentReader进行文件解析。
1.1.1 Resource资源

Spring的配置文件是通过ClassPathResource来封装的,我们来看下他的类关系图:

顶层接口InputStreamSource只提供了一个方法:提供返回InputStream流的方法。

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

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

    存在性exists()。可读性isReadable()。是否处于打开状态isOpen()。

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

    FileSystemResource(文件)。ClassPathResource(ClassPath资源)。UrlResource(URL资源)。InputStreamSource(InputStream资源)。ByteArrayResource(Byte数组)。
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 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语句块中做的事情即可。
}

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

    获取XML文件的验证模式。加载XML文件,并得到对应的document。根据返回的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文档包含:

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

DTD案例:注意DOCTYPE

  
  
  
  
  
XSD

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

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

XSD案例:

  
  
  
  

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

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

其他:

    声明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文档。

SAX解析:逐行扫描文档,一边扫描一边解析。其工作原理简单地说就是:
对文档进行顺序扫描,当扫描到文档(document)开始与结束、元素(element)开始与结束、文档(document)结束等地方时通知事件处理函数,由事件处理函数做相应动作,然后继续同样的扫描,直至文档结束。

我们先来看下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?

如果SAX应用程序需要实现自定义的处理外部实体,则必须实现此接口并使用setEntityResolver方法向SAX驱动器注册一个实例。

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

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

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

publiId:nullsystemId:http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

若读取的是DTD文件:

publiId:-//SPRING//DTD BEAN 2.0//ENsystemId:http://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);
	}
}

主要做三件事:

    创建documentBuilderFactory工厂。工厂创建一个文档构造器documentBuilder。解析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属性,用于在配置文件中指定开发环境,这样可以方便的进行切换开发、部署环境。常用的是更换不同的数据库。
如同一个配置文件中:

xx
xx

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


	Spring.profiles.active
	dev


接下来再看看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处理。其中核心的parseDefaultElement和parseCustomElement方法则在下文继续讲解。

1.3 总结

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

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

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

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

资源读取:

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

资源解析:

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

资源注册:

根据doc来提取和注册Bean。通过DefaultBeanDefinitiondocumentReader的registerBeanDefinitions()方法先处理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文件:



	
		
		
	

测试类:

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,有三步:

    通过ClassPathResource加载对应的xml文件。通过resource实例对象创建出Bean工厂XmlBeanFactory。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加载进来的?又是如何得到我们配置的字段值的?答案也就是源码中核心的parseDefaultElement和parseCustomElement方法。下篇文章则从标签的解析来做具体的展开介绍。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/748755.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号