开篇
本文次要基于 SpringFramework5.2.0.RELEASE
版本,源码的下载步骤在别的文章中曾经讲过,这里就不再赘述。
容器的根本用法
咱们先创立一个简略的示例来看一下容器的根本用法。
创立一个简略的 Java Bean。
/**
* @author 神秘杰克
* 公众号: Java 菜鸟程序员
* @date 2022/3/15
* @Description 简略的 bean 实例
*/
public class MyTestBean {
private String testStr = "testStr";
public String getTestStr() {return testStr;}
public void setTestStr(String testStr) {this.testStr = testStr;}
}
创立一个简略 Spring 配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myTestBean" class="cn.jack.MyTestBean"/>
</beans>
ok,编写一个测试类进行测试。
/**
* @author 神秘杰克
* 公众号: Java 菜鸟程序员
* @date 2022/3/15
* @Description 测试类
*/
@SuppressWarnings("deprecation")
public class BeanFactoryTest {
@Test
public void testSimpleLoad(){final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
final MyTestBean myTestBean = (MyTestBean) beanFactory.getBean("myTestBean");
assertEquals("testStr",myTestBean.getTestStr());
}
}
运行之后能够看到执行胜利,示例就这么简略。
平时咱们并不会间接应用 BeanFactory,而是应用 ApplicationContext,这里只是为了之后能够更好地剖析 Spring 原理。
功能分析
这部分代码实现的性能如下:
- 读取配置文件 beanFactoryTest.xml
- 依据配置文件中的配置找到对应类的配置,并实例化
- 调用实例化后的实例
依照这些形容,咱们能够理解须要至多三个类来实现。
ConfigReader
:用于读取及验证配置文件,随后放到内存中ReflectionUtil
:依据配置文件内容,进行反射实例化,也就是咱们配置文件中的<bean id="myTestBean" class="cn.jack.MyTestBean"/>
App
:用于实现整个逻辑的串联
依照原始的思维形式,整个过程就是这样,然而这么优良的 Spring 框架的构造组成是什么样子?
Spring 的层级架构
咱们首先尝试梳理 Spring 的框架结构,用全局的角度理解 Spring 的形成。
Beans 包的层级架构
实现方才的示例,咱们次要应用的就是 org.springframework.beans.jar。
剖析源码前,咱们首先理解两个外围类。
1.DefaultListableBeanFactory
咱们方才应用的 XmlBeanFactory 继承自 DefaultListableBeanFactory
,而 DefaultListableBeanFactory 是整个加载的外围局部, 是 Spring 注册及加载 bean 的默认实现,而在 XmlBeanFactory 中应用了自定义的 XML 读取器XmlBeanDefinitionReader
,实现了个性化的 BeanDefinitionReader 读取。DefaultListableBeanFactory 继承了 AbstractAutoWireCapableBeanFactory 并实现了 ConfigurableListableBeanFactory 以及 BeanDefinitionRegistry 接口。
从该类图咱们能够清晰理解 DefaultListableBeanFactory 的脉络,咱们先简略理解一下各个类的作用。
AliasRegistry
:定义对 alias 的简略增删改操作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 注册后的解决
XmlBeanFactory 继承 DefaultListableBeanFactory 进行了扩大,次要用于从 XML 中读取 BeanDefinition,对于注册和获取 bean 都是应用从父类 DefaultListableBeanFactory 继承的办法中实现,与父类不同的是 XmlBeanFactory 减少了 XmlBeanDefinitionReader 类型的 reader 属性,进行对资源文件的读取和注册。
2.XmlBeanDefinitionReader
XML 配置文件的读取是 Spring 中重要的性能,咱们能够从 XmlBeanDefinitionReader 中梳理一下资源文件读取、解析及注册的大抵脉络。
咱们首先看一下各个类的性能:
ResourceLoader
:定义资源加载器,次要利用于依据给定的资源文件地址返回对应的 ResourceBeanDefinitionReader
:次要定义资源文件读取并转换为 BeanDefinition 的各个性能EnvironmentCapable
:定义获取 Environment 办法DocumentLoader
:定义从资源文件加载到转换为 Document 的性能AbstractBeanDefinitionReader
:对 EnvironmentCapable、BeanDefinitionReader 类定义的办法进行实现BeanDefinitionDocumentReader
:定义读取 Document 并注册 BeanDefinition 性能BeanDefinitionParserDelegate
:定义解析 Element 的各种办法
通过下面的剖析,咱们能够梳理出 XML 配置文件读取的大略流程。
- 通过继承自 AbstractBeanDefinitionReader 中的办法,来应用 ResourLoader 将资源文件门路转换为对应的 Resource 文件
- 通过 DocumentLoader 对 Resource 文件进行转换,将 Resource 文件转换为 Document 文件
- 通过实现接口 BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对 Document 进行解析,并应用 BeanDefinitionParserDelegate 对 Element 进行解析
容器的根底 XmlBeanFactory
在有了对 Spring 容器的大抵理解后,咱们接下来剖析上面代码的实现。
final BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml"));
通过时序图咱们能够看到首先调用了 ClassPathResource 的构造函数来结构 Resource 资源文件的实例对象,之后的资源解决就能够用 Resource 提供的服务来操作了,之后就能够进行对 XmlBeanFactory 进行初始化了,接下来咱们看一下 Resource 资源是如何封装的。
配置文件封装
首先读取文件是通过 ClassPathResource 进行封装的,比方new ClassPathResource("beanFactoryTest.xml")
,那么 ClassPathResource 实现了什么性能?
在 Java 中,将不同起源的资源形象为 URL,而后注册不同的 handler(URLStreamHandler)来解决不同资源的读取逻辑,然而 URL 没有默认定义绝对 Classpath 或 ServletContext 等资源的 handler,尽管能够通过注册本人的 URLStreamHandler 来解析特定的 URL 前缀协定,然而这须要理解 URL 的实现机制,而且 URL 也没有提供根本的办法(比方资源是否可读、是否存在等)。因此在 Spring 中对其外部应用到的资源实现了本人的形象构造,Resource 接口封装底层资源。
public interface InputStreamSource {InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {boolean exists();
default boolean isReadable() {return exists();
}
default boolean isOpen() {return false;}
default boolean isFile() {return false;}
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
default ReadableByteChannel readableChannel() throws IOException {return Channels.newChannel(getInputStream());
}
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
@Nullable
String getFilename();
String getDescription();}
InputStreamSource 中只有一个办法 getInputStream(),返回一个新的 InputStream 对象。
Resource 接口形象了 Spring 外部应用到的底层资源,File、URL、Classpath 等。定义了 4 个判断资源状态的办法:exists()、isReadable()、isOpen()、isFile(),另外,Resource 接口也提供了不同资源到 URL、URI、File 类型的转换。
createRelative()办法能够基于以后资源创立一个绝对资源的办法。
getDescription()办法用来在错误处理中打印信息。
大抵理解了 Spring 中将配置文件封装为 Resource 类型的实例办法后,咱们持续看 XmlBeanFactory 的初始化过程,咱们这里通过应用 Resource 实例作为结构函数参数的办法。
public XmlBeanFactory(Resource resource) throws BeansException {this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
下面的办法中 this.reader.loadBeanDefinitions(resource);
就是资源加载的真正实现,也是剖析的重点之一。
咱们下面的时序图 3.1 就是这里实现的。在调用到该办法之前,咱们还要调用父类结构器进行初始化。
咱们间接跟踪到父类 AbstractAutowireCapableBeanFactory 的构造函数中:
public AbstractAutowireCapableBeanFactory() {super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
ignoreDependencyInterface 的次要性能是 疏忽给定接口的主动拆卸性能。为什么要这样做?
比方:当 A 中有属性 B,当 Spring 在获取 A 的 bean 的时候如果 B 还没初始化,那么 Spring 会主动初始化 B,然而在某些状况下,B 不会被初始化,其中一个状况就是 B 实现了 BeanNameAware 接口。
Spring 中这样介绍的:主动拆卸时疏忽给定的依赖接口,典型利用就是通过其余形式解析 Application 上下文注册依赖,相似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。
加载 Bean
回到方才,咱们在 XmlBeanFactory 构造函数中调用了 this.reader.loadBeanDefinitions(resource);
这句代码就是整个资源加载的入口,咱们看一下这个办法的时序图。
咱们尝试梳理一下处理过程:
- 应用 EncodedResource 类对参数 Resource 资源文件进行封装
- 从 Resource 中获取对应的 InputStream,随后结构 InputSource
- 随后应用结构的 InputSource 实例和 Resource 实例持续调用 doLoadBeanDefinitions 办法
接下来看一下 loadBeanDefinitions 办法具体实现过程。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {return loadBeanDefinitions(new EncodedResource(resource));
}
首先咱们看一下 EncodedResource 的作用是什么,通过名字能够看进去是对资源文件进行编码解决,次要逻辑就在其中的 getReader() 办法中,如果设置了编码属性,Spring 会应用相应的编码作为输出流的编码。
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());
}
}
该办法结构了一个 InputStreamReader。当结构完 EncodedResource 之后,调用了 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);
}
//resourcesCurrentlyBeingLoaded 是一个 ThreadLocal,外面寄存着 Resource 类的 set 汇合
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 如果 set 中已有这个元素则返回 false 并抛出异样
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException("Detected cyclic loading of" + encodedResource + "- check your import definitions!");
}
try {
// 从 encodedResource 中获取曾经封装的 Resource 对象并再次从 Resource 中获取 InputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 筹备解析 xml 文件,全门路为 org.xml.sax.InputSource
InputSource inputSource = new InputSource(inputStream);
// 设置编码集
if (encodedResource.getEncoding() != null) {inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("IOException parsing XML document from" + encodedResource.getResource(), ex);
}
finally {
// 资源加载结束,移除该 Resource
currentResources.remove(encodedResource);
// 如果没有其余资源了,则 remove
if (currentResources.isEmpty()) {this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
该办法首先将传入的 Resource 参数进行封装,目标是为了思考到 Resource 可能存在编码要求的状况,其次,通过 SAX 读取 XML 文件的形式来创立 InputSource 对象,最初将参数传入到 外围解决局部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 (BeanDefinitionStoreException ex) {throw ex;}
// ... 省略 catch
}
该办法次要做三件事:
- 获取对 XML 文件的验证模式
- 加载 XML 文件,失去对应的 Document
- 依据返回的 Document 注册 Bean 信息
咱们一步步看,从 XML 验证模式开始。
获取 XML 的验证模式
比拟罕用的验证 XML 正确性有两种:DTD 和 XSD。
DTD(Document Type Definition)即文档类型定义,是一种 XML 束缚模式语言,是 XML 文件的验证机制。
DTD 即文档类型定义,是一种 XML 束缚模式语言,是 XML 文件的验证机制, 属于 XML 文件组成的一部分。
DTD 是一种保障 XML 文档格局正确的无效办法,能够通过比拟 XML 文档和 DTD 文件来看文档是否符合规范,元素和标签应用是否正确。一个 DTD 文档蕴含:元素的定义规定,元素间关系的定义规定,元素可应用的属性,可应用的实体或符号规定。
DTD 和 XSD 相比:DTD 是应用非 XML 语法编写的。
DTD 不可扩大,不反对命名空间,只提供十分无限的数据类型。
XSD(XML Schemas Definition),即 XML Schema 语言,针对 DTD 的缺点有 W3C 在 2001 年推出。XML Schema 自身就是一个 XML 文档,应用的是 XML 语法,因而能够很不便地解析 XSD 文档。
绝对于 DTD,XSD 具备如下劣势:
- XML Schema 基于 XML,没有专门的语法
- XML Schema 能够像其余 XML 文件一样解析和解决
- XML Schema 相比于 DTD 提供了更丰盛的数据类型
- XML Schema 提供可扩大的数据模型
- XML Schema 反对综合命名空间
- XML Schema 反对属性组
在 Spring 源码中,基于 XML 文件配置 Bean 的验证模式,个别状况下是 XSD 模式。
验证模式的读取
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;
}
Spring 检测验证模式就是通过判断是否蕴含 DOCTYPE,蕴含就是 DTD,否则是 XSD。
获取 Document
通过验证模式筹备后,就能够进行 Document 加载了,这里的 documentLoader 是一个接口,真正实现是上面的 DefaultDocumentLoader。
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
// 创立文档构建器工厂对象,并初始化一些属性
// 如果验证模式为 XSD,那么强制反对 XML 名称空间,并加上 schema 属性
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
// 依照 XML 文档解析给定 inputSource 的内容,而后返回一个新的 DOM 对象
return builder.parse(inputSource);
}
这段代码次要创立了一个 DocumentBuilderFactory 实例,再通过 DocumentBuilderFactory 创立了一个 DocumentBuilder,最初解析 inputSource 返回 Document 对象。
解析及注册 BeanDefinitions
咱们再回到 doLoadBeanDefinitions 办法,拿到 Document 对象后,咱们就能够注册 bean 对象,调用 registerBeanDefinitions 办法。
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 实例化 BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 记录统计前 BeanDefinition 的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载及注册 bean(要害)documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 记录本次加载个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
调用 registerBeanDefinitions 办法,抉择实现类为DeDefaultBeanDefinitionDocumentReader
。
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
// 拿到了 xml 文档对象的根元素 并调用该办法
doRegisterBeanDefinitions(doc.getDocumentElement());
}
doRegisterBeanDefinitions 开始真正地解析。
protected void doRegisterBeanDefinitions(Element root) {
// 任何被嵌套的 <beans> 元素都会导致此办法的递归。为了正确的流传和保留 <beans> 的默认属性、// 放弃以后(父)代理的跟踪,它可能为 null
// 为了可能回退,新的(子)代理具备父的援用,最终会重置 this.delegate 回到它的初始(父)援用。// 这个行为模仿了一堆代理,但实际上并不需要一个代理
BeanDefinitionParserDelegate parent = this.delegate;
// 代码(1)this.delegate = createDelegate(getReaderContext(), root, parent);
// 默认名称空间是 "http://www.springframework.org/schema/beans"
if (this.delegate.isDefaultNamespace(root)) {String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
// 在 xml 配置文件中对 profile 的设置 辨别是生产环境还是线上环境
if (StringUtils.hasText(profileSpec)) {String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
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;
}
}
}
// 解析前解决,留给子类实现
preProcessXml(root);
// 生成 BeanDefinition,并注册在工厂中,代码(2)parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现
postProcessXml(root);
this.delegate = parent;
}
咱们先看一下代码(1)的办法,该办法创立了一个 BeanDefinitionParserDelegate 对象,该对象是对 XML 中属性值解析的委派。
protected BeanDefinitionParserDelegate createDelegate(XmlReaderContext readerContext, Element root, @Nullable BeanDefinitionParserDelegate parentDelegate) {BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
delegate.initDefaults(root, parentDelegate);
return delegate;
}
咱们看下 BeanDefinitionParserDelegate 类的常量。
public class BeanDefinitionParserDelegate {
public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
public static final String MULTI_VALUE_ATTRIBUTE_DELIMITERS = ",;";
/**
* Value of a T/F attribute that represents true.
* Anything else represents false. Case seNsItive.
*/
public static final String TRUE_VALUE = "true";
public static final String FALSE_VALUE = "false";
public static final String DEFAULT_VALUE = "default";
public static final String DESCRIPTION_ELEMENT = "description";
public static final String AUTOWIRE_NO_VALUE = "no";
public static final String AUTOWIRE_BY_NAME_VALUE = "byName";
public static final String AUTOWIRE_BY_TYPE_VALUE = "byType";
public static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor";
public static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect";
public static final String NAME_ATTRIBUTE = "name";
public static final String BEAN_ELEMENT = "bean";
public static final String META_ELEMENT = "meta";
public static final String ID_ATTRIBUTE = "id";
public static final String PARENT_ATTRIBUTE = "parent";
//...
}
咱们发现 Spring 配置文件的属性全都在这里 当初咱们晓得 BeanDefinitionParserDelegate 对象的确是来对 XML 配置文件的解析后,持续回到 createDelegate 办法,创立了 BeanDefinitionParserDelegate 对象后,还执行了 initDefaults 办法,来初始化一些默认值。
解析并注册 BeanDefinition
当初咱们回到代码(2),进入 parseBeanDefinitions 办法。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 对 beans 的解决,默认名称空间是 "http://www.springframework.org/schema/beans"
if (delegate.isDefaultNamespace(root)) {
// 获取根元素下的子 Node
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);
if (node instanceof Element) {
// 拿到了 <beans> 下的子标签
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 如果该标签属于 beans 的名称空间,则进入这个办法
//xmlns="http://www.springframework.org/schema/beans"
parseDefaultElement(ele, delegate);
}
else {
// 如果该标签属于其余的名称空间比方:context,aop 等
//xmlns:aop="http://www.springframework.org/schema/aop"
//xmlns:context="http://www.springframework.org/schema/context"
delegate.parseCustomElement(ele);
}
}
}
}
else {delegate.parseCustomElement(root);
}
}
在 Spring 中的 XML 分为两大类,一种是默认的如下:
<bean id="test" class="cn.jack.Test"/>
另一种就是自定义的:
<tx:annotation-driven/>
如果是自定义实现的话,则须要用户实现自定义配置,如果根节点或者节点是应用默认命名的话则应用 parseDefaultElement 进行解析,否则应用 delegate.parseCustomElement 办法对自定义命名空间进行解析。
而判断是默认命名还是自定义命名空间则应用 isDefaultNamespace 办法中的 node.getNamespaceURI()获取命名空间,随后与固定的命名空间 http://www.springframework.org/schema/beans
进行比照,不统一则为自定义命名空间。
对于默认标签解析与自定义标签解析则在下一篇文章中。