前面已经大体理解了加载Bean的那个过程
- 进行编码
- 使用ThreadLocal来避免循环加载配置文件
- 获取Dom信息并注册
获取Dom信息并注册
在doLoadDocument方法里面就是获取Dom信息
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
可以看到这个方法,使用了documentLoader去实际加载指定的文档
这里面还调用了一个方法getValidationModeForResource
这个方法的作用是获取对XML的验证模式!
获取XML的验证模式
XML的验证模式保证了XML文件的正确性,比较常用的验证模式主要有两种,DTD和XSD
DTO和XSD验证模式的区别
DTO,称为文档类型定义,是一种XML约束模式语言,同时也是XML的验证机制,验证的机制是通过比较XML文档和DTD文件来看文档是否符合规范
XSD,本身就是XML语言(Xml Scheme Definition),使用一个指定的XMl Scheme来验证某个XML文档,来检查该XML文档是否符合其要求,即XML Scheme可以限制XML文档所允许的结构和内容,并根据此检查XML文档是否有效
而验证模式的定义使用,是放在配置文件的头描述里面的
比如使用XSD
使用DTD格式的
验证模式的获取
获取验证模式,其实就是XmlBeanDefinitionReader调用了getValidationModeForResource方法
下面就来看看这个方法的底层
getValidationModeForResource
protected int getValidationModeForResource(Resource resource) {
//获取手动设置的验证模式(XmlBeanDefinitionReader可以设置validationMode)
//也就是从代码层面上进行设置
int validationModeToUse = getValidationMode();
//如果验证模式不是未指定的,就返回获取的验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
//如果获取的验证模式是未指定,则需要自动的检测(检测配置文件)
int detectedMode = detectValidationMode(resource);
//如果自动检测后得到的验证模式不是未指定的,返回
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
//如果配置文件上依然没有指定验证模式
//默认使用XSD模式(注释上写的原因是,因为找不到DTD所以使用XSD)
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
return VALIDATION_XSD;
}
从这个方法来看,设置验证模式可以有两种方式
- 通过XmlBeanDefinitionReader来获取validationMode,即从代码上设置validationMode,并且优先级最高
- 通过配置文件去获取validationMode,优先级最低
- 如果两个都没有设置,默认使用XSD验证模式
配置文件去获取validationMode(detectValidationMode方法)
对应的方法就是detectValidationMode方法
protected int detectValidationMode(Resource resource) {
//判断配置文件抽象成的resoure是不是open状态的
//如果处于open状态,则不能从该配置文件上读取验证模式
//因为open状态,表示已经被打开了
//被打开了就只能被读取一次然后马上关闭,因为可能会造成资源泄露
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.");
}
//获取文件的io流
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);
}
//具体的获取验证模式的功能又是交由validationModeDetector去执行
try {
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);
}
}
可见XmlBeanDefinitionReader判断了resource是否可用之后,调用validationModeDector对象去获取校验模式
ValidationModeDector的detectValidationMode方法
在这个方法里面才是真正地读取配置文件去获取校验模式的细节!!!!
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
// 通过读取配置文件去寻找DOCTYPE,DOCTYPE就是配置文件中指定DTD校验模式的前缀
// 所以这也体现了一句话,约定大于配置大于编码
// 从这也可以估计到,这里是通过寻找DTD,找不到DTD就使用XSD。。。
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
//这一步其实判断当前行是不是注释
content = consumeCommentTokens(content);
//如果这一行为注释,此时content为null,则读取下一行,这一行不处理
// inComment代表当前解析位置处于注释里面
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
//判断当前解析位置是否有DOCTYPE 如果有DOCTYPE就是DTD校验模式
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
// 如果有DOCTYPE,为DTD校验格式,如果没有,则是XSD校验格式
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
//对于一些不支持的编码,就将主动权返回给调用者去处理。。。。
return VALIDATION_AUTO;
}
}
从源码上可以看出,通过配置文件去获取校验模式,除了不支持编码问题,通过逐行解析文本,如果碰到了DOCTYPE字样,就代表是DTD模式,如果没有碰到,就代表是XSD模式
获取校验模式后,接下来就返回上一层的XmlBeanDefinitionReader去解析Document了
解析Document
也就是使用documentLoader方法去执行loadDocument方法去获取Document对象,可以看到,documentLoader其实是一个接口,而且只有一个实现类DefaultDocumentLoader,并且里面只有一个方法就是loadDocument
所以,我们直接进入DefaultLoadDocument
DefaultLoadDocument
下面就看看那个实现的loadDocument方法
@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);
}
其实这个loadDocument方法是使用标准的JAXP配置的XML解析器去简单地加载XML文档的,与使用SAX解析XML套路大致都一样
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是干什么的
EntityResolver的作用是提供一个如何寻找DTD、XSD声明的方法,也就是由程序来实现寻找校验模式的声明
下面来看一下这个接口
这个接口提供了唯一一个方法,这个方法就是定义去哪里寻找校验模式的
解析及注册BeanDefinitions
经过DefaultLoadDocument进行解析后,现在已经为XML配置文件生成了一棵DOM树了,即Document实体,此时就回到了XmlBeanDefinitionReader的doLoadBeanDefinitions方法
下一步就是进行注册BeanDefinitions了,也就是执行registerBeanDefinitions方法
registerBeanDefinitions方法
这个方法就是用来提取以及注册Bean的
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//使用DefaultBeanDefinitionsReader来实例化BeanDefinitionDocumentReader接口
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//记录BeanDefinitions的加载个数(即之前加载的个数)
int countBefore = getRegistry().getBeanDefinitionCount();
//加载和注册配置文件的Bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次加载配置文件的Bean的个数并返回
return getRegistry().getBeanDefinitionCount() - countBefore;
}
在这个方法中,注册Bean交给了BeanDefinitionDocumentReader去做,而XmlBeanDefinitionReader只关心配置文件进行加载和转化的工作,很好地应用了面向对象的单一职责的原则,将逻辑处理都委托给单一的类进行处理,也就是交由BeanDefinitionDocumentReader去做
下面就来看看BeanDefinitionDocumentReader是怎么注册Bean的
BeanDefinitionDocumentReader进行注册Bean
源码如下
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
//注册的细节还在doRegisterBeanDefinitions里面
//可以看到给的参数为Dom里面的DocumentElement,也就是最上层标签,Dom树的根节点!
doRegisterBeanDefinitions(doc.getDocumentElement());
}
doRegisterBeanDefinitions方法
源码如下
下面就是真正的注册Bean的过程了!
/**
* Register each bean definition within the given root {@code <beans/>} element.
*/
@SuppressWarnings("deprecation") // for Environment.acceptsProfiles(String...)
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
//装饰者模式,BeanDefinitionDocumentReader装饰了BeanDefinitionParserDelegate
//实际上进行XML解析的是BeanDefinitionParserDelegate去做的(单一职责)
//而BeanDefinitions主要做的是处理profiles属性
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
//判断给定的节点是否是默认的命名空间
//命名空间是XML里的说法,命名空间可以理解为导入资源
//只有引入了命名空间才可以使用对应的标签
//默认命名空间为bean标签,alias标签和import标签
//这里是加载Bean,必须使用到Bean标签,所以一定要有引入默认的命名空间
//因为如果是这个命名空间,证明使用bean标签进行加载!!
//Spring才知道要怎样进行解析
if (this.delegate.isDefaultNamespace(root)) {
//处理profile属性(也就是对应环境,dev环境还是produce环境)
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
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了
//这个是个空方法,代表处理XML前要做的事情(留给了子类去实现,面向继承而设计的思想,模板方法!)
preProcessXml(root);
//开始处理XML
parseBeanDefinitions(root, this.delegate);
//这个也是个空方法,代表解析完XML后要做的事情(留给了子类去实现,面向继承而设计的思想,模板方法!)
//如果后面要对开始处理XML做特殊处理的时候,可以让子类去实现这两个方法,子类正常调用该方法即可
postProcessXml(root);
this.delegate = parent;
}
从代码上可以看到,有以下重要几点
-
BeanDefinitionDocumentReader采用了模板方法、面向继承去设计,对XML进行解析的前后处理交由子类去实现
-
BeanDefinitionDocumentReader装饰了BeanDefinitionParserDelegate,本身自己主要负责Profiles属性的处理,而真正的解析XML是要靠BeanDefinitionParserDelegate做的,很好地体现了单一职责原则
profile属性的应用
估计很少人在Bean上很少用这个属性,但是在SpringBoot的配置文件肯定很多人使用spring.profiles = dev去对应设置配置文件对应的环境,其实在bean标签上也是可以设置profiles属性的,表明Bean在什么环境下才会被激活
下面是来自官方文档的示例
而且这里要注意的是,profiles是可以指定多个的,下面就看看如何处理profiles的
if (this.delegate.isDefaultNamespace(root)) {
//注意,这里处理的是根节点的profiles,相当于是整个容器的profile
//获取bena标签内profile属性对应的value
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
//判断profile属性有没有对应的value
if (StringUtils.hasText(profileSpec)) {
//这里是解析value的,因为profile可以指定多个,使用逗号分开
//这里的MULTI_VALUE_ATTRIBUTE_DELIMITERS代表分界符的意思,为常量逗号
//所以,这里的specifiedProfiles字符串数组存储了所有的环境
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
//下面是判断解析出来的profiles是不是都符合环境变量中定义的,
//如果不符合定义就不会浪费性能(在启动Spring就要指定启动的环境)
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;
}
}
}
下面来看看怎么进行匹配环境的
@Override
@Deprecated
public boolean acceptsProfiles(String... profiles) {
Assert.notEmpty(profiles, "Must specify at least one profile");
//遍历所有的profiles
for (String profile : profiles) {
//如果某个profile开头是!,为只要不是这个环境就进行加载
if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
//去除掉!
//判断当前环境是否不对应这个环境
//如果不对应环境,返回true代表可以进行下面的加载!
if (!isProfileActive(profile.substring(1))) {
return true;
}
}
//如果不是以!开头,就是代表这个环境才尽心加载
//判断当前环境是否对应这个环境,对应的就返回True,可以进行下一步加载
else if (isProfileActive(profile)) {
return true;
}
}
return false;
}
再看看如何判断环境是否对应的吗,下面是源码
protected boolean isProfileActive(String profile) {
//这一步只是校验传进来的profile而已
//具体校验有两种情况
//一种是,开头还是!(因为传进来时已经去掉感叹号了,所以不应该存在感叹号)
//另外一种是为空,为空还判断个P
validateProfile(profile);
//取出现在激活的环境,使用的是一个Set集合存储的
//具体来说还是一个LinkedHashSet
Set<String> currentActiveProfiles = doGetActiveProfiles();
//判断当前激活环境包含传进来的profile,如果包含,代表环境对应
//如果当前环境为空,那就参考代表以默认环境启动
//所以判断默认环境是否包含传进来的profile,如果包含代表环境对应
return (currentActiveProfiles.contains(profile) ||
(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}
校验传进来的profile的源码!!!
总结以下,这个解析profile分为以下几个步骤
-
取传进来的root节点的profiles属性
-
解析profiles属性,因为profiles属性是可以指定多个的,并且用逗号划分,所以根据逗号将多个profile属性放进数组里面
-
判断当前激活的环境是否包含profile,使用遍历(激活环境使用LinkedHashSet来存储的)
-
先校验传进来的profile是否符合贵方
- 不能为空
- 不能有!(感叹号传进来时已经去掉了)
-
如果激活的环境为空,代表没有配置,使用默认的环境,以默认环境去判断是否包含profile
-
激活环境不为空,就以激活环境为准
-
-
如果profile没有一个对应的,那就直接return了,不会进行下一步的注册Bean了
解析并注册BeanDefinition
处理完Profiles后,并且有profile对应激活环境,那么现在就要开始解析和注册BeanDefinition了
前面提到过,注册BeanDefinition是交给beanDefinitionParseDelegate去做的
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) {
Element ele = (Element) node;
//同样去判断注册的Bean是不是也为默认的命名空间
//如果开启了默认的命名空间
//parseDefaultElement来进行注册
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
//如果不是默认的命名空间
//parseCustomElement进行注册
else {
delegate.parseCustomElement(ele);
}
}
}
}
//如果Root,不是默认的命名空间
//同理去使用parseCutomElement进行注册
else {
delegate.parseCustomElement(root);
}
}
可以看到,针对命名空间,Spring提供了两种注册方式
这是因为Spring的XML配置里面有两大类Bean声明,一个是默认的就是Bean标签,另外一种就是开启注解支持在代码上进行Bean声明,因为两种的解析差异是比较大的,所以要分开去注册