Spring XML Bean 的解析

DTD(Document Type Definition)文档类型定义—是XML文档的一部分,XSD(XML Scheme Definition)XML 规范定义 --也是一份XML文件

Spring XML Bean 的解析

默认标签的解析:

Import、 alias、 bean 和 beans

bean标签解析

bean 的 name 首先取 定义的 id,如果id未定义且存在别名列表,就取第一个别名作为 bean 的 name,如果根据id 和别名列表都没有找到 bean 的名称则根据 Spring bean 名称的生成规则生成该 bean 的名称。

bean的 id 和 别名必须在一个容器中唯一,检查唯一性不通过则打印错误日志,不报错;注意检查通过后 bean 的 name和别名都会被加入到 已经使用的 bean 名称集合中,下次检查下一个 bean 的时候使用。

BeanDefinitionHolder 包括 beanDefinition beanName和别名数组

new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray)

创建BeanDefinition 后,首先根据根据 className 设置 className或 Class 如果有 类加载器不为空,直接获取类设置Class,如果加载器为空则只设置 className

解析 XML中定义的 bean元素的 scope属性 设置 BeanDefinition ,如果当前 元素中未定义 scope且存在父级元素,则使用父级别元素定义的 scope 属性值作为当前 元素解析后的 BeanDefinition 的scope

Lazy-init属性没有设置或设置为 除 true外的其他字符串都被视为 该属性为 false

constructor-arg 该元素的子元素 只能有一个,超过一个则直接报错,注意:description和 meta属性元素除外

constructor-arg 的子元素最多一个,且不能和 constructor-arg 的属性 ref 或 value 共存。三者之能有一个存在

ref :RuntimeBeanReference

value : TypedStringValue

subElement: 子元素的解析有专门的方法解析--而不是直接返回 RuntimeBeanReference 或 TypedStringValue 子元素可以是:非默认空间的元素—调用嵌套自定义元素解析器解析,bean(parseBeanDefinitionElement 返回 BeanDefinitionHolder),ref(返回 RuntimeBeanReference ),idref(返回 RuntimeBeanNameReference),value( 该子元素中可以使用 type属性指定值的类型;返回 TypedStringValue),null(返回 TypedStringValue),array(该元素可以使用 value-type指定集合中元素类型,返回 ManagedArray),list(该元素可以使用 value-type指定集合中元素类型,返回 ManagedList),set(该元素可以使用 value-type指定集合中元素类型,返回 ManagedSet),set(map一定有字元素 entry 该子元素又可以使用 key key-ref value value-ref,,该元素可以使用 key-type指定集合key元素类型,使用 value-type指定集合value元素类型,返回 ManagedMap),props(一定有子元素 prop 该子元素又一定有 属性 key,返回ManagedProperties)

ConstructorArgumentValues

ValueHolder implements BeanMetadataElement
public interface BeanMetadataElement {

   /**
    * Return the configuration source {@code Object} for this metadata element
    * (may be {@code null}).
    */
   Object getSource();

}

属性指定 index 或未指定 index (type,name属性可选)

private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<Integer, ValueHolder>(0);

private final List<ValueHolder> genericArgumentValues = new LinkedList<ValueHolder>();

元素的子元素的 property 必须要有 name 属性,且同一个元素中的不同子元素 property name不能重复,

property这个子元素的 嵌套子元素 或 属性 ref 或value 不能共存,接下来返回的就和 constructor-arg 这个子元素一样了。

只是最后这些信息这里是封装在了 PropertyValue 中

qualifier 子元素 用于指定唯一注入的 bean 属性有 type 指定 bean 的 class 且 type属性必须有,value属性可以选择性有,可能的嵌套子元素:attribute 且如果有嵌套子元素 attribute 则嵌套子元素必须包含 key ,value属性。信息封装到这个对象中 AutowireCandidateQualifier

RootBeanDefinition 原始 配置解析的 BeanDefinition ,存在嵌套的则使用 RootBeanDefinition 和ChildBeanDefinition

GenericBeanDefinition 是一站式的 BeanDefinition 是更新版本中加的,AbstractBeanDefinition 则是三者的抽象 实现了所有共同的功能。

默认 bean 标签中嵌套的自定义标签或属性解析

获取默认标签中的所有属性,所有子元素遍历找到不是默认命名空间的属性或元素,针对每一个自定义属性或元素获取对应的自定义命名空间,根据命名空间找到解析器解析自定义属性和标签后进行装饰。

alias标签的解析

<alias name="dataSource" alias="dataa"/>  
<alias name="dataSource" alias="datab"/> 

获取 beanName,获取alias;获取 beanName和 alias 这两个字段都不能为空,为空则跳过不注册,如果需要注册就将 alias 作为 key. beanName作为 value注册。

条件判断递归调用:

public boolean hasAlias(String name, String alias) {
   for (Map.Entry<String, String> entry : this.aliasMap.entrySet()) {
      String registeredName = entry.getValue();
      if (registeredName.equals(name)) {
         String registeredAlias = entry.getKey();
         return (registeredAlias.equals(alias) || hasAlias(registeredAlias, alias));
      }
   }
   return false;
}

import 标签解析

<import resource="classpath*:XXXXX.xml">

首先获取 resource 属性,该属性必须有,没有则直接返回不做进一步加载解析处理。

解析resource 属性 中的 系统属性变量,如: "${user.dir}"替换为配置的常量

判断 resource 属性 中配置的文件地址是URL还是相对路径

下列情况就是URL:

classpath*: , classpath: 开头的

能构建 URL的

new URL(resourceLocation)

如果是URL 则递归调用 bean 的解析过程

首先判断当前 ResourceLoader 是不是 ResourcePatternResolver,如果是则表示按照 通配符匹配合适的 Resource;

首先根据 resource 属性配置的 location 查找匹配上的 资源:

如果是classpath* 的则找到确定的根目录,查找根目录下的所有 资源,然后按照 location中 的 通配符匹配需要的资源返回

classpath*:spring/**/spring-load.xml -> classpath*:spring/            **/spring-load.xml*
classpath*:spring/spring-load.xml-> classpath*:spring/spring-load.xml 
classpath*:spring/*/spring-load.xml -> classpath*:spring/         */spring-load.xml
  • protocol: vfs

  • JarURL:jar,zip,vfszip,wsjar

  • 其他都是文件系统

if (resourceLoader instanceof ResourcePatternResolver) {
   // Resource pattern matching available.
   try {
      Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
      int loadCount = loadBeanDefinitions(resources);
      if (actualResources != null) {
         for (Resource resource : resources) {
            actualResources.add(resource);
         }
      }
      if (logger.isDebugEnabled()) {
         logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
      }
      return loadCount;
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "Could not resolve bean definition resource pattern [" + location + "]", ex);
   }
}

否则是当前文件系统的相对路径

先构建出相对资源,然后再匹配资源进行加载解析注册

自定义标签解析

先介绍如何实现自己的自定义标签

一般我们要实现自己的较为复杂的配置的时候,最原始的做法就是用原生态的方式去解析定义好的XML文件,然后转换为配置对象,但是这个方式的缺点是自己必须用原生态的方式去解析XML配置文件,实现起来比较麻烦;还有一种方式就是扩展Spring 定义的 Schema。第二种方式的实现需要以下几个步骤:

  • 创建一个需要扩展的组件
  • 定义一个XSD文件描述组件内容
  • 创建一个文件,实现BeanDefinitionParser 接口,用来解析 XSD文件中的定义和组件定义
  • 创建一个Handler文件,扩展自 NamespaceHandlerSupport,目的是将组件注册到 Spring容器
  • 编写 Spring.handlers 和 Spring.shcema文件

示例如下:

  • 编写 一个POJO 对象,用于接收解析配置文件得到的 Bean对象

    import lombok.Data;
    
    @Data
    public class User {
    
        private String userName;
    
        private String email;
    }
    
  • 定义一个XSD文件描述组件内容——该文件放在对应 自定义Handler 所在项目模块的 根目录下的 META-INF/文件夹下

    <?xml version="1.0" encoding="UTF-8"?>
    <schema xmlns="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://jbpm.org/4.2/user"
            xmlns:tns="http://www.lexueba.com/schema/user"
            elementFormDefault="qualified">
        <element name="user">
            <complexType>
                <attribute name="id" type="string"/>
                <attribute name="userName" type="string"/>
                <attribute name="email" type="string"/>
            </complexType>
        </element>
    </schema>
    

    在上面的 XSD 文件中描述了一个新的 targetNamespace, 并在这个空间中定义了一个 name 为user的element, user有3个属性id、 userName和email,其中email的类型为string。 这3个类主要用于验证Spring配置文件中向定义格式。 XSD文件是XMLDTD的替代者,使用XML在Schema 语言进行编写

  • 创建一个文件 ,实现 BeanDefinitionParser接口,用来解析 XSD 文件中的定义和组件定义

    import org.springframework.beans.factory.support.BeanDefinitionBuilder;
    import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
    import org.springframework.util.StringUtils;
    import org.w3c.dom.Element;
    
    public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    
        @Override
        protected Class<?> getBeanClass(Element element) {
            return User.class;
        }
    
        @Override
        protected void doParse(Element element, BeanDefinitionBuilder builder) {
            String userName = element.getAttribute("userName");
            String email = element.getAttribute("email");
            if (StringUtils.hasText(userName)) {
                builder.addPropertyValue("userName", userName);
                if (StringUtils.hasText(email)) {
                    builder.addPropertyValue("email", email);
                }
            }
        }
    }
    
    
  • 创建一个 Handler 文件,扩展 自 NamespaceHandlerSupport,目的是将组件注册到Spring 容器 。

    public class CustomeNamespaceHandler extends NamespaceHandlerSupport {
        @Override
        public void init() {
            registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
        }
    }
    

    以上代码很简单, 无非是当遇到向定义标签<user:aaa这样类似于以 user开头的元素,就会把这个元素扔给对应的 UserBeanDefinitionParser去解析。

  • 编写 Spring.handlers 和 Spring.schemas 文件, 默认位置是在对应工程Module 的META-INF/文件夹下(Spring项目加载首次加载所有Handler的时候会去META-INF/spring.handlers 这个配置文件中加载),当然,你可以通过 Spring 的扩展或者修改源码的方式改变路径 。

到这里,自定义 的配置就结束了,而 Spring加载自定义的大致流程是遇到自定义标签然后就去 Spring.handlers 和 Spring.schemas 中去找对应的 handler 和 XSD,默认位置是/META-INF/下,进而有找到对应的 handler以及解析元素的 Parser,从而完成了整个自定义元素的解析,也就是说向定义与 Spring 中默认的标准配置不同在于 Spring 将向定义标签解析的工作委托给了用户去实现 。

自定义标签解析源码解读

首先获取自定义标签元素的命名空间---org.w3c.dom.Node中已经提供了方法供 我们直接调用

根据命名空间找到对应自定义元素的 NameSpaceHandler,所有 NameSpaceHandler 都存储在一个全局的 Mapping中 ,如果首次获取 NameSpaceHandler 的时候这个Map为空,则到指定路径下的配置文件中加载所有Handler,默认配置在如下路径的配置文件中,配置文件中配置的 key 是 nameSpcaceUri,value是 Handler 的全路径 类名称,首次初始化完成后Mapping中存储的 key 就是配置的 nameSpaceUri,value就是累的全路径;在根据 nameSpaceUri 获取对应Handler的时候 如果发现 value类型不是 NamespaceHandler 的 ,则使用类加载器加载对应的类并进行实例化后 ,并调用 对应的 init方法初始化该 Handler 注册的 Parser,再 put回map

public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";

自定义的NameSpaceHandler中实现继承类 NamespaceHandlerSupport 中的 init 方法,在这个方法中会将该 NameSpace 空间下的所有自定义标签的解析器 注册 完成,注册是调用父类 NamespaceHandlerSupport 的 方法 registerBeanDefinitionParser 完成的,这个方法 会将解析器 put 进维护所有解析器的 map中。

private final Map<String, BeanDefinitionParser> parsers = new HashMap();

然后调用 NameSpaceHandler 的parse方法,该方法是 这些 Handler都必须继承的父类 NamespaceHandlerSupport 中的方法,这个方法会 根据元素和 NameSpace找到注册的 解析器,调用用户自己实现的解析器的 parse方法解析元素(如果自定义元素解析器是直接实现接口 BeanDefinitionParser 的话 )但是我们一般 都是继承 AbstractSingleBeanDefinitionParser 或 AbstractBeanDefinitionParser (如果是继承前者的话我们需要重写 doParse 方法 如果继承后者我们需要重写 parseInternal 方法)。AbstractSingleBeanDefinitionParser 继承自 AbstractBeanDefinitionParser ,AbstractBeanDefinitionParser 的 parse 方法使用了 模版方法 模式,预留了 parseInternal 抽象方法 给 子类自己实现;AbstractSingleBeanDefinitionParser 就是继承了 AbstractBeanDefinitionParser 重写了 预留的 parseInternal 方法,AbstractSingleBeanDefinitionParser 重写的 parseInternal 方法又实用了模版方法模式,预留了 doParse 抽象方法给 自类实现,所有我们 如果是单例的 Bean 的解析,直接继承 AbstractSingleBeanDefinitionParser 重写 doParse 方法;如果不是的话可以继承 AbstractBeanDefinitionParser 实现 parseInternal 方法,而最复杂的是直接实现接口 BeanDefinitionParser,如Dubbo就是直接实现了该接口,没有继承任何抽象类。

分析了源码后我们再来看 调用链路,如果 我们直接实现接口的话,parse方法就直接调用我们自己 的方法,如果是继承的是 AbstractSingleBeanDefinitionParser 或者 AbstractBeanDefinitionParser 的话,则调用的是 AbstractBeanDefinitionParser 的模版方法 parse 。

一个 NameSpace下面可以有多个 自定义元素解析器,都在 Handler 的 init 方法中进行注册

配置文件中如下的配置

<dubbo:application name="spring-dubbo-nacos-provider"  />

dubbo 就是 NameSpace application 就是对应的自定义元素。还有 dubbo:service,dubbo:registry 等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343