目录
Spring配置文件的解析逻辑
整个流程概览
- 首先新建一个Maven项目(参照序),配置好spring依赖,maven下载好源码和doc。
随便建一个类TestBean
public class TestBean { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
resources里建一个fullflow.xml
<?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-3.1.xsd"> <bean class="TestBean" id="testBean"/> </beans>
测试类
public class TestFullFlow { public static void main(String[] args) { BeanFactory beanFactory = new XmlBeanFactory( new ClassPathResource("fullflow.xml")); System.out.println(beanFactory.getBean("testBean") != null); } }
跟进XmlBeanFactory构造器
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }
第二行
this.reader.loadBeanDefinitions(resource);
就是我们主要要看的。
跟进代码XmlBeanDefinitionReaderpublic int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); }
这里有个类叫EncodedResource,目的是把资源封装为有编码的,就是把编码和资源绑定在一块。
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.getResource()); } //获得正在加载的资源文件 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { //第一次为空,初始化 currentResources = new HashSet<EncodedResource>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } //如果添加当前资源文件不成功,因为是Set(不可重复集合),代表已经有了这个文件 //抛出异常(循环加载) if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { InputStream inputStream = encodedResource.getResource().getInputStream(); try { InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } //这个才是主要的逻辑,spring的源码风格,一般以do开头的才是主要做事的。 return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } finally { inputStream.close(); } } …… }
跟进
doLoadBeanDefinitions
方法:int validationMode = getValidationModeForResource(resource); Document doc = this.documentLoader.loadDocument( inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware()); return registerBeanDefinitions(doc, resource);
下面我们分别看这几个方法的逻辑
XML Validator模式的判断
跟踪代码到XmlBeanDefinitionReader类
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; }
validationMode定义为
private int validationMode = VALIDATION_AUTO;
跟踪到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; 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(); } }
consumeCommentTokens方法的作用是消耗注释,让我们一起看看这个逻辑
private String consumeCommentTokens(String line) { //首先如果没有带<!-- 或 -->的直接返回内容代表没有再注释里面 if (line.indexOf(START_COMMENT) == -1 && line.indexOf(END_COMMENT) == -1) { return line; } while ((line = consume(line)) != null) { //没有在注释中或者不是由注释开头的返回内容,inComment的这个标识位表示当前是否在注释中 if (!this.inComment && !line.trim().startsWith(START_COMMENT)) { return line; } } return line; }
跟踪consume方法
private String consume(String line) { //如果当前在注释中,消费注释结尾标记-->,否则消耗<!-- int index = (this.inComment ? endComment(line) : startComment(line)); return (index == -1 ? null : line.substring(index)); } //注:inComment这个标识位主要是为了多行注释处理。 private int startComment(String line) { //如果找到注释开头标记,把inComment设为true return commentToken(line, START_COMMENT, true); } private int endComment(String line) { //如果找到注释结尾标记,把inComment设为false,代表这个注释块结束 return commentToken(line, END_COMMENT, false); } /** * Try to consume the supplied token against the supplied content and update the * in comment parse state to the supplied value. Returns the index into the content * which is after the token or -1 if the token is not found. */ private int commentToken(String line, String token, boolean inCommentIfPresent) { //如果找到注释标记 int index = line.indexOf(token); if (index > - 1) { this.inComment = inCommentIfPresent; } //得到去掉注释后内容的开头index return (index == -1 ? index : index + token.length()); }
消耗注释的逻辑就是这样,接下来看主方法detectValidationMode
private static final String DOCTYPE = "DOCTYPE"; private boolean hasDoctype(String content) { return (content.indexOf(DOCTYPE) > -1); }
如果找到了DOCTYPE定义,那么就是DTD的,否则就是Schema模式。
关于xml验证模式的推定逻辑到此结束。
XML验证文件的URL解析
回到XmlBeanDefinitionReader的doLoadBeanDefinitions方法
Document doc = this.documentLoader.loadDocument(inputSource,getEntityResolver(),this.errorHandler,validationMode,isNamespaceAware());
这里可以知道spring的xml解析使用的Java xml api里的DOM。
让我们看getEntityResolverprotected 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是个什么概念呢?我们定义的XML文件里有引用相关的DTD和XSD,它们都是一个URL。那么一般会从网络上下载这些DTD和XSD,但是当网络不好的时候或者断网的时候就体验不好或无法做到。这里使用了EntityResolver解析本地的DTD和XSD(Schema)。
这里有两种EntityResolver:ResourceEntityResolver和DelegatingEntityResolver,而实际上前者是后者的子类。前者的作用是如果DelegatingEntityResolver加载不到资源,那么再使用已设置的ResourceLoader尝试加载资源。让我们看一看DelegatingEntityResolver的实现。public DelegatingEntityResolver(ClassLoader classLoader) { //这里有两种Resolver,一个是DTD的一个是Schema的,spring会根据参数systemId(一个XML定义的参数)来选择使用。 this.dtdResolver = new BeansDtdResolver(); this.schemaResolver = new PluggableSchemaResolver(classLoader); }
BeansDtdResolver内部在jar包里找spring-beans-2.0.dtd和spring-beans.dtd。
而PluggableSchemaResolver内部则是找jar包里的META-INF/spring.schemas
文件,里面是Schema文件(xsd)对应在jar包里的路径。通过这个来解析我们再spring配置文件里定义的xsd。
注册BeanDefinition
上述把获取验证方式和spring xml的验证文件的获取逻辑讲完了,也就是下面代码的第一二句。
int validationMode = getValidationModeForResource(resource); Document doc = this.documentLoader.loadDocument(inputSource, getEntityResolver(),this.errorHandler, validationMode,isNamespaceAware()); return registerBeanDefinitions(doc, resource);
下面到了
registerBeanDefinitions(doc, resource);
这一句。跟进public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { //获取BeanDefinitionDocumentReader ,默认是DefaultBeanDefinitionDocumentReader BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); //设置当前环境到Reader里 documentReader.setEnvironment(this.getEnvironment()); //获取当前的BeanDefinition数量 int countBefore = getRegistry().getBeanDefinitionCount(); //注册此次的BeanDefinition documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); //得到此次注册的数量 return getRegistry().getBeanDefinitionCount() - countBefore; }
跟进到
DefaultBeanDefinitionDocumentReader
protected void doRegisterBeanDefinitions(Element root) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { Assert.state(this.environment != null, "Environment must be set for evaluating profiles"); String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!this.environment.acceptsProfiles(specifiedProfiles)) { return; } }
这一段是解析spring的profile属性,关于profile的用法,请自行查找资料。总的来说是和maven的profile差不多,也是切换各个环境的配置(dev、product)等。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { 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)) { //这里是处理默认命名空间的标签 parseDefaultElement(ele, delegate); } else { delegate.parseCustomElement(ele); } } } } else { //这里是处理自定义标签的 delegate.parseCustomElement(root); } }
在spring中有两种标签,一个是beans命名空间下的默认标签:
<bean id="bean1" class="TestBean"/>
,第二种是自定义标签<aop:aspect id="myAspect" ref="aBean">
像这种的,又如像dubbo的自定义标签等。parseCustomElement
都是用来处理这种标签的
自带命名空间标签的解析
自带默认命名空间的解析
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { //解析import标签 importBeanDefinitionResource(ele); } else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { //解析alias标签 processAliasRegistration(ele); } else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { //解析bean标签 processBeanDefinition(ele, delegate); } else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse //解析嵌套beans标签 doRegisterBeanDefinitions(ele); } }
import标签的解析
protected void importBeanDefinitionResource(Element ele) { …… //解析占位符 location = environment.resolveRequiredPlaceholders(location); boolean absoluteLocation = false; …… //判断是否是绝对路径 absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); …… else { // No URL -> considering resource location as relative to the current file. try { //相对路径加载 int importCount; Resource relativeResource = getReaderContext().getResource().createRelative(location); if (relativeResource.exists()) { //这里的loadBeanDefinitions又回到了先前的方法里,是一个环形调用 importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); actualResources.add(relativeResource); } else { String baseLocation = getReaderContext().getResource().getURL().toString(); importCount = getReaderContext().getReader().loadBeanDefinitions( StringUtils.applyRelativePath(baseLocation, location), actualResources); } }
其他解析alias等的就不介绍了,最重要的方法是
processBeanDefinition
,解析bean标签的protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate){ //委托给了BeanDefinitionParserDelegate 加载 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); }
到
BeanDefinitionParserDelegate
的parseBeanDefinitionElement
方法parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); parseMetaElements(ele, bd); parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); parseConstructorArgElements(ele, bd); parsePropertyElements(ele, bd); parseQualifierElements(ele, bd);
Bean标签的解析过程
首先看上述第一句
//解析Bean标签的属性 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
跟进
if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { // Spring 2.x "scope" attribute bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { //这一句表示不能同时有scope和singleton error("Specify either 'scope' or 'singleton', not both", ele); } } else if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { // Spring 1.x "singleton" attribute bd.setScope(TRUE_VALUE.equals(ele.getAttribute(SINGLETON_ATTRIBUTE)) ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE); }
这里提一句,spring的singleton属性是1.x版本的,在2.x标准的是使用
scope="singleton"
。3.x版本的需要兼顾以上两者,但是这两个属性是互斥的,不能同时指定。下面的代码就是对spring的bean标签的各个属性的解析了,如
abstract
,depends-on
,lazy-init
,dependency-check
,autowire
等等的解析了。对spring各属性不熟的同学可以查一查资料,不在本文讲述范围内。