配置文件封装
我们都知道spring的配置文件读取是通过ClassPathResource进行封装的。
new ClassPathResource("xxxxx.xml")
那么这个类做了什么呢?
我们先看看它的目录结构
我们直接找到它的顶级父类AbstractResource 发现它实现了Resource接口
观察一下Resource
Resource接口抽像了所有Spring内部使用到的底层资源:File、URL、classpath等。定义了判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外它还提供了不同资源到URL、URI、File类型的转换。
好了现在直接看一下Resource接口底下有多少类被实现
好家伙,全是熟悉的类
先简单看一下ClassPathResource源码里是怎么实现的
private final String path;
@Nullable
private ClassLoader classLoader;
@Nullable
private Class<?> clazz;
public ClassPathResource(String path) {
this(path, (ClassLoader) null);
}
//这里直接通过class或者classLoader提供的底层方法进行调用
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
嗯,再看看FileSystemResource的源码
public InputStream getInputStream() throws IOException {
try {
return Files.newInputStream(this.file.toPath());
}
catch (NoSuchFileException ex) {
throw new FileNotFoundException(ex.getMessage());
}
}
先说结论:各种Resource类最后都会把资源加载成为各种InputStream…
好了,现在就看看XmlBeanFactory的初始化过程了。看看这里面是如何对InputStream进行处理的,直接上源码。
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
原来是通过XmlBeanDefinitionReader类来对资源加载的呀。
具体如何加载Bean的呢?
在XmlBeanFactory里调用了XmlBeanDefinitionReader类型的reader属性提供的方法loadBeanDefinitions,它的功能是对资源的加载。
那么现在就来分析一下XmlBeanDefinitionReader类里都做了些啥
一打开这个类的源码,我擦,好多代码。不急,先找到之前调用的loadBeanDefinitions方法先。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
这里先小小得重载了一下并把Resource资源进行了编码处理
好,我们现在看重载后的方法具体怎么写的
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource);
}
//定义了一个set来保存resource
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
//把resource里的inputStream拿出来放到核心方法doLoadBeanDefinitions里
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
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 {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
这里主要就是定义了一个set来保存resource,然后从封装好的encodedResource对象中获取其中的inputStream然后再调用doLoadBeanDefinitions方法,看来这个方法才是真正的核心。
直接上源码
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
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);
}
}
这里可以看到这里做了两件事:
1、调用了doLoadDocument方法把资源加载成了Document对象
2、根据返回的Document去注册Bean信息
其实这里还做了一件事儿,获取对XML文件的验证模式
直接看doLoadDocument方法里
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
这里可以注意到getValidationModeForResource方法,它的作用是获取对XML文件的验证模式。
那么这个验证模式是啥呢…已经凌晨3点了,明天再学吧。
XmlBeanDefinitionReader类里的getValidationModeForResource方法用来获取资源的验证模式,那么验证模式又是什么?
比较常用的验证模式有两种:DTD和XSD
DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。
要使用DTD验证模式的时候需要在XML文件的头部声明
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
XSD(XML Schemas Definition)描述了XML文档的结构。可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。
在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间外(xmlns=“http://www.springframework.org/schema/beans”)还必须指定该名称空间所对应的XML Schema文档的存储位置(xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd“)
验证模式的读取
先来看看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;
}
// 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;
}
再来看看自动检测方法detectValidationMode的源码
protected int detectValidationMode(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 {
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);
}
}
好家伙,这里又调用了XmlValidationModeDetector的detectValidationMode方法,再上源码
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
boolean isDtdValidated = false;
String content;
//spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是 XSD
while ((content = reader.readLine()) != null) {
content = consumeCommentTokens(content);
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
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();
}
}
这个方法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。
获取Document
经过了验证模式的步骤就可以进行Document加载了,读取文档的方法并没有放在XmlBeanFactoryReader类里,这里使用了DocumentLoader的实现类DefaultDocumentLoader去执行
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource来返回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到底是做什么用的呢?
EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。
对于不同的验证模式,Spring使用了不同的解析器解析。
比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载
解析及注册BeanDefinitions
先回顾一下之前的doLoadBeanDefinitions方法里干了什么
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
当程序已经拥有XML文档文件的Document实例对象时,就会调用下面这个registerBeanDefinitions方法
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//使用BeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//记录统计前BeanDefinition的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
//加载及注册bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//记录本次加载的BeanDefinition个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
引用书中的话:
其中的参数doc是通过loadDocument加载转换出来的。在这个方法中很好地应用了面向对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
来了,它来了,doRegisterBeanDefinitions()方法经过艰难险阻到了我们面前!
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.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
//解析前处理,留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
//解析后处理,留给子类实现
postProcessXml(root);
this.delegate = parent;
}
上面的流程就是先对profile进行处理,然后开始解析,然后看preProcessXml()和postProcessXml()时发现是空方法,原来是留给子类重写的做解析前后的处理。
那profile是干嘛用的呢?
在xml配置文件里对bean加上profile属性可以用来配置来适用于生产环境和开发环境
<beans profile="dev"></beans>
<beans proflie="production"></beans>
//集成到Web环境中时,在web.xml中加入以下代码
<context-param>
<param-name>Spring.profiles.active</param-name>
<param-value>dev</param-value>
</context-param>
了解了profile的使用再来分析代码会清晰很多,首先程序会获取beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不会浪费性能去解析。
解析并注册BeanDefinition
处理了profile后就可进行XML读取看看parseBeanDefinitions方法如何实现的
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//对beans的处理
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;
if (delegate.isDefaultNamespace(ele)) {
//对bean的处理
parseDefaultElement(ele, delegate);
}
else {
//对bean的处理
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
在Spring的XML配置里面有两大类Bean声明,一个是默认的,如:
<bean id="test" class="test.TestBean"/>
另一类就是自定义的,如:
<tx:annotation-driven/>
这两种方式的读取及解析差别是非常大的,对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate.parseCustomElement方法对自定义命名空间进行解析。