0
点赞
收藏
分享

微信扫一扫

容器的基本实现

Mhhao 2022-04-22 阅读 55

spring容器的基本实现

本人用的spring版本为5.1.4.RELEASE

1.我们通过这串代码来debug查看spring容器创建的流程

	public static void main(String[] args) {
		BeanFactory factory = new XmlBeanFactory(new ClassPathResource("mytest.xml"));
        Object person = factory.getBean("person");
	}
我们可以想到上述代码执行的大致流程
  1. 加载mytest.xml文件
  2. 读取mytest.xml文件,并且转换成实例
  3. 将实例交由factory管理
  4. 通过beanName获取到person实例(可以猜到这里的实例应该是由map管理的)

2.读取源码之前的准备工作

核心类的介绍
  1. DefaultListableBeanFactory
DefaultListableBeanFactory的类层次结构图

在这里插入图片描述

DefaultListableBeanFactory的类图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0qXq8Yz-1650585339852)(D:\javalearning\后端\markdown-image\image-20220417091216232.png)]

从类图和层次结构图中我们可以很清楚的看到DefaultListableBeanFactory的脉络,我们先简单理解下这上面各个类的大致作用

  • AliasRegistry:定义对alias简单增删改等操作(alias从名字就可以知道是bean的别名了)
  • SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口AliasRegistry进行实现
  • SingletonBeanRegistry:定义对单例的注册及获取。
  • BeanFactory:定义获取bean及bean的各种属性
  • DefaultSingletonBeanRegistry:对接口SingletonBeanRegistry各函数的实现
  • HierarchicalBeanFactory:继承BeanFactory,也就是在BeanFactory定义的功能的基础上增加了对parentFactory的支持
  • BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作
  • FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理
  • ConfigurableBeanFactory:提供配置Factory的各种方法
  • ListableBeanFactory:根据各种条件获取bean的配置清单
  • AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurableBeanFactory的功能
  • AutowireCapableBeanFactory:提供创建bean,自动注入,初始化以及应用bean的后处理器。
  • AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现
  • ConfigurableListableBeanFactory:BeanFactory的配置清单,指定忽略类型及接口等。
  • DefaultListableBeanFactory:综合上面所有功能,主要是对bean注册后的处理。

同样的Xml配置文件的读取是spring中的重要功能(虽然现在基本上用的是注解方式),我们先从XmlBeanDefinitionReader中梳理一下资源文件的读取,解析及注册

在这之前我们先考虑,从xml中读取文件并且转换成BeanDefinition需要哪几个步骤吧

  • 很显然,我们要读取xml首先要进行对xml文件的加载(ResourceLoader)
  • 加载完成之后,我们需要将xml文件转换成document,有学习过js的同学应该都知道Document吧?我们可以对Document进行各种操作(比如通过id来获取对应的标签啥的)
  • 将获取到的Document解析成为一个BeanDefinition对象。

那现在我们来看一下spring都定义了上面类来实现这些功能吧

  • ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource。

  • BeanDefinitionReader:主要定义资源文件读取并转换成BeanDefinition的各个功能

  • EnvironmentCapable:定义获取Environment方法

  • DoucumentLoader:定义从资源文加载到转换为Document的功能。

  • AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现。

  • BeanDefinitionDocumentReader:定义读取Document并注册BeanDefinition功能。

  • BeanDefinitionParserDelegate:定义解析Element的各种方法

通过以上分析,我们可以梳理出整个XML配置文件读取的大致流程入下所示,在XmlBeanDefinitionReader主要包含以下几步的处理

在这里插入图片描述

  • 通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourceLoader将资源文件路径转换成为对应的Resouce文件。

  • 通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件。

  • 通过实现接口BeanDefinitionDocumentReader中的DefaultBeanDefinitionDocument类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析。

3.容器的基础xmlBeanFactory

我们首先来看这段代码执行了什么操作

BeanFactory factory = new XmlBeanFactory(new ClassPathResource("mytest.xml"));
  • 通过ResourceLoader将mytest.xml文件转换为Resource对象
  • 将resource对象传入到XmlBeanFactory的构造方法当中
  • 处理resource,通过loadBeanDefinitions,返回给BeanFactory

在这里插入图片描述

3.1配置文件封装

​ Spring的配置文件读取是通过ClassPathResource进行封装的,如new ClassPathResource(“myTest.xml”),那么ClassPathResource完成了什么功能呢?

​ 在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的资源逻辑,一般handler的类型使用不同前缀(协议,Protocol)来识别,如"file:" “http:” "jar:"等,然而URL没有默认定义相对ClassPath或ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如”classpath“,然而这需要了解URL的实现机制,而且URL也没有提供基本的方法,如检查当前资源是否存在,检查当前资源是否可读等方法。因而Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口封装底层资源。

Resource接口如下

public interface Resource extends InputStreamSource {
    
	// 判断该文件是否存在
	boolean exists();

	// 表示资源是否可读
	default boolean isReadable() {
		return exists();
	}
    
	// 表示此资源是否表示具有打开流的句柄,如果为true表示,InputStream不能多次读取,必须读取并关闭以避免内存泄漏问题
	default boolean isOpen() {
		return false;
	}

	// 确定此资源是否代表文件系统的文件
	default boolean isFile() {
		return false;
	}

	// 获取该Resource的URL
	URL getURL() throws IOException;

	// 返回该Resource的URI
	URI getURI() throws IOException;

	// 返回该Resource的文件
	File getFile() throws IOException;
    
	// 返回一个可读字节channel
	default ReadableByteChannel readableChannel() throws IOException {
		return Channels.newChannel(getInputStream());
	}

	// 返回此资源的内容长度
	long contentLength() throws IOException;

	// 确定此资源最后修改的时间戳
	long lastModified() throws IOException;

	// 创建与此资源相关的资源
	Resource createRelative(String relativePath) throws IOException;

	// 获取该资源的name
	@Nullable
	String getFilename();

	// 返回该资源的描述
	String getDescription();

}

InputStreamSource接口

public interface InputStreamSource {
   // 获取流入流
   InputStream getInputStream() throws IOException;

}

我们在日常使用中,资源文件加载也是经常用到的,我们可以直接使用Spring提供的类,比如我们如果希望加载文件时,可以使用如下代码

Resource resource = new ClassPathResouceInputStream is = resource.getInputStream();
// classPathResource 在import org.springframework.core.io.ClassPathResource;包下

得到inputstream我们就可以按照以前的开发方式进行实现了,并且我们可以利用Resource及其子类为我们提供诸多特性。

有了Resource接口我们就可以对所有资源文件进行统一处理。至于实现,我们以getInputStream为例子,ClassPathResource中的实现方式是通过class或者classLoader提供的底层方法进行调用

ClassPathResource.java

	@Override
	public InputStream getInputStream() throws IOException {
		InputStream is;
        // 如果class不为null
		if (this.clazz != null) {
            // 通过getResourceAsStream方法获取输入流
			is = this.clazz.getResourceAsStream(this.path);
		}
        // 如果类加载器不为null
		else if (this.classLoader != null) {
            // 同样的通过getResourceAsStream方法获取输入流
			is = this.classLoader.getResourceAsStream(this.path);
		}
		else {
            // 如果都为null的情况,直接调用getSystemResourceAsStream获取流
			is = ClassLoader.getSystemResourceAsStream(this.path);
		}
		if (is == null) {
			throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
		}
		return is;
	}

知道了这些我们来看下spring具体是怎么实现加载Resource的吧

  1. 进入ClassPathResource的构造器方法
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("custom-tag.xml"));
  1. 调用含有参数path,与classLoader的构造器方法
	public ClassPathResource(String path) {
		this(path, (ClassLoader) null);
	}
  1. 进入该构造器方法
	public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
		// 判断path是否为null,如果为null则抛出"Path must not be null"异常
		Assert.notNull(path, "Path must not be null");
		// 将path规范化
		String pathToUse = StringUtils.cleanPath(path);
		// 如果path为‘/’开头,删除
		if (pathToUse.startsWith("/")) {
			pathToUse = pathToUse.substring(1);
		}
		// 将pathToUse赋值给path
		this.path = pathToUse;
		// 如果classLoader为null,则我们获取java的默认类加载器即(ApplicationClassLoader)
		// 这里插一嘴jvm的内容,jvm自带三个类加载器
		// bootstrapClassLoader,引导类加载器。用来加载java的核心库
		// Extension ClassLoader,拓展类加载器。用来加载从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(拓展目录)下加载类库
		// Application ClassLoader 应用程序加载器,一般我们写的类都是由该加载器加载
		this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
	}

了解了Spring中将配置文件封装为Resource类型的实例方法后,我们就可以继续探寻XmlBeanFactory的初始化过程,XmlBeanFactory的初始化有若干方法,Spring中提供了许多构造函数,在这里分析的就是Resource实例作为构造函数参数的方法。

3.2加载bean
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
		super(parentBeanFactory);
		// 这里是真正调用加载BeanDefinition的操作。
     	// 通过前面的UML图我们找到,xmlBeanDefinitionReader的主要操作就是
    	// 将resource加载为Document对象
    	// 使用BeanDefinitionParserDelegate将element解析成BeanDefinition
		this.reader.loadBeanDefinitions(resource);
	}

在深入这个方法之前,我们先大致了解一下loadBeanDefinitions干了些什么事

  • 将resource封装为encodedResource
  • 调用loadBeanDefinitions的重载方法
  • 通过resource方法获取inputStream
  • 将inputStream封装成为inputSource
  • 如果encodedResource的编码不为null,则将编码设置给inputSource
  • 将inputSource与resource传入doLoadBeanDefinitions,开始真正的解析操作

接下来我们来看下源码吧

首先

	public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		// 对resource使用EncodedResource进行封装
        // 很显然我们可以通过指定的编码,来作为resource的流的编码
		return loadBeanDefinitions(new EncodedResource(resource));
	}

进入loadBeanDefinitions的重载方法中

	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<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
		if (currentResources == null) {
			currentResources = new HashSet<>(4);
			this.resourcesCurrentlyBeingLoaded.set(currentResources);
		}
		// 将该被encode的资源存放在currentResources资源当中
		if (!currentResources.add(encodedResource)) {
			throw new BeanDefinitionStoreException(
					"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
		}
		try {
			// 通过encodeResource获取对应的资源的InputStream
			InputStream inputStream = encodedResource.getResource().getInputStream();
			try {
				// 将InputStream封装成InputSource
				InputSource inputSource = new InputSource(inputStream);
				// 如果编码不为null,则将编码的值赋值给inputSource
				if (encodedResource.getEncoding() != null) {
					inputSource.setEncoding(encodedResource.getEncoding());
				}
				// 准备工作结束开始真正解析BeanDefinition了
				return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
			}
			finally {
				inputStream.close();
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"IOException parsing XML document from " + encodedResource.getResource(), ex);
		}
		finally {
			currentResources.remove(encodedResource);
			if (currentResources.isEmpty()) {
				this.resourcesCurrentlyBeingLoaded.remove();
			}
		}
	}

通过源码很容易发现其实loadBeanDefinitions方法,只是完成了加载BeanDefinition的准备操作

  • 将Resource封装为EncodedResource
  • currentResources 存放已经加载过的资源,如果存在循环依赖资源的情况,抛出异常,即a.xml 需要导入 b.xml,b.xml 需要导入 a.xml
  • 通过Resource对象获取InputStream
  • 将InputStream封装成为InputSource
  • 如果编码不为null,则将编码的值赋值给inputSource
  • 执行doLoadBeanDefinition(inputSource,Resource)做真正的BeanDefinition加载操作

好那么在进入doLoadBeanDefinitions方法之前,我们先想一下doLoadBeanDefinitions,将会干什么吧

  • 将Resource文件解析成Doucument
  • 将Doucument注册成为BeanDefinition

那我们来看下doLoadBeanDefinitions的代码吧(XmlBeanDefinitionReader类中)

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

		try {
			// 将resource解析成document
			Document doc = doLoadDocument(inputSource, resource);
			// 注册BeanDefinition
			int count = registerBeanDefinitions(doc, resource);
			if (logger.isDebugEnabled()) {
				logger.debug("Loaded " + count + " bean definitions from " + resource);
			}
			return count;
		}
		catch (BeanDefinitionStoreException ex) {
			throw ex;
		}
		catch (SAXParseException ex) {
			throw new XmlBeanDefinitionStoreException(resource.getDescription(),
					"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
		}
		catch (SAXException ex) {
			throw new XmlBeanDefinitionStoreException(resource.getDescription(),
					"XML document from " + resource + " is invalid", ex);
		}
		catch (ParserConfigurationException ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"Parser configuration exception parsing XML from " + resource, ex);
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"IOException parsing XML document from " + resource, ex);
		}
		catch (Throwable ex) {
			throw new BeanDefinitionStoreException(resource.getDescription(),
					"Unexpected exception parsing XML document from " + resource, ex);
		}
	}

4.加载Document

接下来我们重点来看下spring是怎么将resource封装成为document的吧

	protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		/**
		 * getEntityResolver()
		 */
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}

4.1获取resource的验证模式

在讲验证模式之前,就不得不说一下xml两种验证模式了,xsd与dtd

要使用dtd模式就先要在xml文件中声明

在这里插入图片描述

而同样的xsd就是

在这里插入图片描述

大致懂了这个之后,我们进行接下来的获取验证模式的操作

protected int getValidationModeForResource(Resource resource) {
		// 获取对应的验证模式,可以通过XmlBeanDefinitionReader中的setValidationMode()方法设置
		int validationModeToUse = getValidationMode();
		// 如果validationModeToUse不为1,即默认值。则返回我们之前设置的验证模式的值
		if (validationModeToUse != VALIDATION_AUTO) {
			return validationModeToUse;
		}
		// 获取到对应的验证模式
		int detectedMode = detectValidationMode(resource);
		if (detectedMode != VALIDATION_AUTO) {
			// 如果detecteMode不为默认值则返回对应值
			return detectedMode;
		}
		// 默认的验证模式为XSD
		return VALIDATION_XSD;
	}

这段代码大致执行的就是

  • 先获取验证模式,查看用户是否提前指定了验证模式。
  • 如果有则直接返回用户提前指定的验证模式
  • 没有,则通过resource进行验证模式的获取
  • 如果获取到的验证模式不为默认值,则直接返回
  • 为默认值则返回XSD的验证模式

在进入通过resource获取验证模式之前,我们可以先猜猜spring是怎么做的。很显然应该是如下操作

  • 通过resource来获取输入流
  • 按行读取输入流
    • 如果读取到xxx.dtd返回dtd的验证模式
    • 如果读取到xxx.xsd返回xsd的验证模式

进入detectValidationMode(resource)方法查看

	protected int detectValidationMode(Resource resource) {
		// 如果该resource已经被读,则抛出异常
		if (resource.isOpen()) {
			throw new BeanDefinitionStoreException(
					"Passed-in Resource [" + resource + "] contains an open stream: " +
					"cannot determine validation mode automatically. Either pass in a Resource " +
					"that is able to create fresh streams, or explicitly specify the validationMode " +
					"on your XmlBeanDefinitionReader instance.");
		}

		InputStream inputStream;
		try {
			// 获取到输入流
			inputStream = resource.getInputStream();
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
					"Did you attempt to load directly from a SAX InputSource without specifying the " +
					"validationMode on your XmlBeanDefinitionReader instance?", ex);
		}

		try {
			// 通过XmlValidationModeDetector,来解析inputstream获取到对应的验证模式
			return this.validationModeDetector.detectValidationMode(inputStream);
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
					resource + "]: an error occurred whilst reading from the InputStream.", ex);
		}
	}

在真正读取之前,spring还干了些准备操作

  • 查看resource是否已经存在被读取状态

spring把主要的实现逻辑放在了detectValidationMode的重载方法当中,而这个重载方法是在XmlValidationModeDetector类当中的。

我们写代码时,也应该要像spring这样,每个类尽量只干一件事(单一职责),我们可以通过聚合等方法,来对别的功能进行调用。

XmlValidationModeDetector类

	public int detectValidationMode(InputStream inputStream) throws IOException {
		// Peek into the file to look for DOCTYPE.
		// 将inputStream封装成一个BufferReader
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		try {
			// 判断是否为dtd验证(默认为false)
			boolean isDtdValidated = false;
			String content;
			// 读取xml中每一行的信息
			while ((content = reader.readLine()) != null) {

				content = consumeCommentTokens(content);
				// 如果该content不在xml的解析内容里(即注释),或者content不存在数据时,继续读取
				if (this.inComment || !StringUtils.hasText(content)) {
					continue;
				}
				// 如果包含DOCTYPE则返回DTD的验证模式
				if (hasDoctype(content)) {
					isDtdValidated = true;
					break;
				}
				// 如果以<打头,验证模式一定是在< 之前
				if (hasOpeningTag(content)) {
					// End of meaningful data...
					break;
				}
			}
			// 返回对应的解析xml模式
			return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
		}
		catch (CharConversionException ex) {
			// Choked on some character encoding...
			// Leave the decision up to the caller.
			// 解析时出现异常,则返回默认值
			return VALIDATION_AUTO;
		}
		finally {
			// 关闭资源
			reader.close();
		}
	}

看到这个代码跟我猜的还是有点出入的,即spring就只判断了是否读取到DOCTYPE,如果有则返回dtd,如果读取到<打头,且紧跟着字母,我们就知道改xml文件必定不是dtd模式了,那么就是xsd

我们看下hasOpeningTag(content)方法

	private boolean hasOpeningTag(String content) {
		if (this.inComment) {
			return false;
		}
		int openTagIndex = content.indexOf('<');
		return (openTagIndex > -1 && (content.length() > openTagIndex + 1) &&
				// 判断<后面是否为字母
				Character.isLetter(content.charAt(openTagIndex + 1)));
	}
4.2 获取Document

经过了验证模式准备的步骤就可以进行Document加载了,同样XmlBeanFactoryReader类对于文档读取也是没有亲力亲为,而是委托给了DocumentLoader,而DocumentLoader是接口,真正调用的DefaultDocumentLoader。代码如下

	@Override
	public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
		// 获取DocumentBuilderFactory的factory
		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);
	}

通过SAX解析XML文档的套路大致都差不多,就是通过DocumentFactory来创建出DocumentBuilder,进而解析inputStream,返回Document对象。大家有兴趣的话可以去网上获取这方面的资料

下面我们就讲下entityResolver的用处吧

传入的entityResolver是通过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的用处到底是什么呢?

4.3EntityResolver的用法

​ 在loadDocument方法中涉及一个参数EntityResolver,那么说明是EntityResolver?官网是这样解释的:如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法向SAX驱动器注册一个实例。说人话就是,SAX读XML文件时,我们先去寻找相应的dtd,然后可以对文档进行一个验证。默认的寻址操作就是通过dtd上相应的URI地址来通过网络下载。而通过网络下载,可能出现网络中断导致下载失败的问题,使得报相应的dtd没有被找到。

​ EntityResolver的作用就是项目本身可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们讲DTD文件放到了项目某处,在实现时直接讲此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。

​ 我们首先来看下EntityResolver接口

在这里插入图片描述

​ 如果我们的验证模式为xsd则这两个参数为
在这里插入图片描述

​ 如果我们验证模式为dtd,这两个参数为
在这里插入图片描述

在这里插入图片描述

那么接下来我们看下EntityResolver是如何做到将这个URL转换到自己的资源路径的

public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
    
   if (systemId != null) {
       // 如果systemId,以.dtd结尾
      if (systemId.endsWith(DTD_SUFFIX)) {
         return this.dtdResolver.resolveEntity(publicId, systemId);
      }
       // 如果systemId,以.xsd结尾
      else if (systemId.endsWith(XSD_SUFFIX)) {
          // 通过调用META-INF/Spring.schemas解析
         return this.schemaResolver.resolveEntity(publicId, systemId);
      }
   }
   return null;
}

在这里插入图片描述

通过debug我们可以知道EntityResolver默认的location地址为META-INF/spring.schemas

再进入解析方法

PluggableSchemaResolver类

	public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Trying to resolve XML entity with public id [" + publicId +
					"] and system id [" + systemId + "]");
		}

		if (systemId != null) {
            // 通过map映射来获取本地对应的解析xsd文件解析
			String resourceLocation = getSchemaMappings().get(systemId);
			if (resourceLocation != null) {
				Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
				try {
					InputSource source = new InputSource(resource.getInputStream());
					source.setPublicId(publicId);
					source.setSystemId(systemId);
					if (logger.isTraceEnabled()) {
						logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
					}
					return source;
				}
				catch (FileNotFoundException ex) {
					if (logger.isDebugEnabled()) {
						logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
					}
				}
			}
		}
		return null;
	}

5.解析及注册BeanDefinitions

把文件转换为Document后,我们回到doLoadBeanDefinitions方法当中(XmlBeanDefinitionReader)

当程序拥有了xml文档文件的Document实例对象时,就会被引入下面这个方法

xmlBeanDefinitionReader类

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    
   // 创建对应的BeanDefinitionDocumentReader的实例,DefaultBeanDefinitionDoucumentReader
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    
   // 获取到这次解析xml文件之前,BeanDefinition的数量
   int countBefore = getRegistry().getBeanDefinitionCount();
    
   // 传入document,注册成为BeanDefinition
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    
   // 返回注册BeanDefinition的个数
   return getRegistry().getBeanDefinitionCount() - countBefore;
}

其中的参数doc,是通过loadDocument加载转换出来的。在这个方法中也很好应用了单一职责原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正类型其实就是DefaultBeanDefinitionDoucmentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册

接下来我们进入DefaultBeanDefinitionDocumentReader的registerBeanDefinitions的方法当中来把

	@Override
	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        // 传过来的XmlReaderContext是由Document封装而成的
		this.readerContext = readerContext;
        // 提取root,并进行真正的注册操作
		doRegisterBeanDefinitions(doc.getDocumentElement());
	}

那么我们就来看下doRegisterDefinitions这个方法吧

	@SuppressWarnings("deprecation")  // for Environment.acceptsProfiles(String...)
	protected void doRegisterBeanDefinitions(Element root) {
		// 获取BeanDefinitionParserDelegate对象
        // 上文我们提到过,BeanDefinitionParseDelegate干的事情就是对Element进行解析操作
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);
		// 如果说这个xml的命名空间是spring中默认的
		if (this.delegate.isDefaultNamespace(root)) {
            // 处理profile属性
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					if (logger.isDebugEnabled()) {
						logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
								"] not matching: " + getReaderContext().getResource());
					}
					return;
				}
			}
		}
		// 解析xml文件之前的操作 通过子类实现
		preProcessXml(root);
		// 真正解析element,element是由BeanDefinitionParserDelegate解析的
		parseBeanDefinitions(root, this.delegate);
		// 在解析xml之后干的事情 通过子类实现
		postProcessXml(root);

		this.delegate = parent;
	}

通过上面的代码我们可以看到处理流程,首先是对profile的处理,然后开始进行解析,可是当我们跟进preProcessXml(root),或者postProcessXml(root)发现代码是空的,既然是空的写着还有什么用呢?就像面向对象设计方法学中常说的一句话,一个类要么是面向继承设计的,要么就用final修饰。在DefaultBeanDefinitionDocumentReader中并没有用final修饰,所以它是面向继承而设计的。这两个方法正是为子类而设计的,如果大家了解设计模式,可以很快反应过来这个是模板方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么就只需要重写这两个方法就可以了。

5.1 profile属性的使用

  • profile属性可以指定你的生产环境,比如在dev环境下你需要一个数据库,在production环境下你又需要另外一个数据库,这时候,你就可以通过profile属性来对要加载的bean进行切换

我们来看下官方给的用法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HnaLBDpE-1650585339859)(D:\javalearning\后端\markdown-image\image-20220421155148418.png)]

​ 有了这个特性我们就可以同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行切换开发,部署环境,最常用的就是更换不同的数据库。

​ 了解了profile的使用在来分析代码会清晰得多,首先程序会获取beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个得,需要程序对其拆分,并解析每个profile都符合环境变量中所定义得,不定义则不会浪费性能去解析。

接下来我们进入到重头戏

parseBeanDefinitions(root, this.delegate);
	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
		// 是否是spring中的默认命名空间,如果是则走if里面
		if (delegate.isDefaultNamespace(root)) {
			// 获取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)) {
                        // 解析默认命名空间的标签
						parseDefaultElement(ele, delegate);
					}
					else {
                        // 解析自定义命名空间的标签
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		// 否则则走另外一个
		else {
			delegate.parseCustomElement(root);
		}
	}

在解析过程中分为了默认命名空间得解析和自定义命名空间得解析。

比如

这个就为spring中默认标签

tx:annotation-driven/

这个就是自定义的标签

由于篇幅有限 在之后的文章继续发布自定义标签的解析和默认标签的解析

大家一起学习一起努力
是否是spring中的默认命名空间,如果是则走if里面
if (delegate.isDefaultNamespace(root)) {
// 获取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)) {
// 解析默认命名空间的标签
parseDefaultElement(ele, delegate);
}
else {
// 解析自定义命名空间的标签
delegate.parseCustomElement(ele);
}
}
}
}
// 否则则走另外一个
else {
delegate.parseCustomElement(root);
}
}


在解析过程中分为了默认命名空间得解析和自定义命名空间得解析。

比如

<bean id="person" class="xxx.xxxx.xx.Person">

这个就为spring中默认标签

<tx:annotation-driven/>

这个就是自定义的标签



由于篇幅有限 在之后的文章继续发布自定义标签的解析和默认标签的解析

大家一起学习一起努力
举报

相关推荐

0 条评论