Spring扩展点之-NameSpaceHandler

Spring标签的黑魔法

如果你JAVAer,那么肯定对Spring的XML的各种标签的配置比较熟悉。我们先来复习一下,所谓温故知新吗。

  1. 当需要声明式事务支持的时候
<tx:annotation-driven/>  //就是这么easy
  1. 当需要Spring自动扫描Bean的时候
<context:component-scan base-package="xxx.xxx" />
  1. 当需要把数据库的配置单独放在一个properties中的时候
<context:property-placeholder location="classpath:/xxx.properties"/>

如果你是初学Spring的话,你很可能被这些稀奇古怪的标签搞晕了头。因为这种配置实在是太多了。而且很可能在之前是那么配置的,过一段时间之后就那么配置了。这里举一个将数据库配置放在properties中的例子。

  • 旧的配置
<bean id="propertyPlaceholderConfigurer" class="org.springframework,beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <value>jdbc.properties<value/>
        </list>
    </property>
</bean>
  • 新的配置
<context:property-placeholder location="classpath:/jdbc.properties"/>

你可能会有疑问,为什么新的版本里可以这么配,难道底层的实现变了吗?

这里先搁置该疑问,后面再一一解答。

其实Spring中还有好多这种带黑魔法的配置,虽然很黑科技,这里就不一一列举了。毕竟它是Spring!

其他开源框架和Spring整合的标签的黑魔法

如果说Spring中自带的黑魔法配置你不以为然,那么其他开源框架中的黑魔法配置又是怎么一回事呢?

先来看几个开源框架的Spring中的配置

1.mongodb的java客户端

    <mongo:mongo id="mongo" host="${mongo.host}" port="${mongo.port}">
      <mongo:options
         connections-per-host="${mongo.connectionsPerHost}"
         threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}"
         write-fsync="${mongo.writeFsync}"/>
    </mongo:mongo>

2.携程开源的配置管理中心 点击这里

<apollo:config/>

3.当当开源的分布式任务调度框架elastic-job

    <job:dataflow id="icSyncBaseDataJob" class="xxx.xxxx" streaming-process="true" registry-center-ref="regCenter" cron="0 0 2 * * ?" sharding-total-count="3" overwrite="true">
        <job:listener class="xxx.xxxx" />
    </job:dataflow>

可以看到每一个开源框架都有自己独有的命名空间(NameSpace),有没有感觉很吊!

那么作为一个有理想有抱负的程序员,你肯定会想如果有一天我自己开源了一个框架,并且要和Spring做集成的时候,我自己是不是也可以自定义属于自己的Spring标签(命名空间)呢?

如何自定义Spring的标签

自定义标签的流程我这里就不详细些了,这里推荐下面几篇文章

自定义Spring标签的原理

看完上面的自定义标签的流程,你是不是有疑问,为什么这样做就可以定义自己的标签。

其实这都要归功于Spring在设计之初就一贯坚持的设计原则:开闭原则:对扩展开放(Open for extension),对修改关闭(Closed for modification)。所以你可以在不修改Spring源代码的情况下扩展Spring的功能。

下面我们我们就来看一下Spring对应XML标签的处理

public class DefaultBeanDefinitionDocumentReader{
        // 解析xml标签
    /**
     * Parse the elements at the root level in the document:
     * "import", "alias", "bean".
     * @param root the DOM root element of the document
     */
    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;
                   // Spring默认的bean配置
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                  // 用户自定义的bean配置
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
      // 用户自定义的bean配置
        else {
            delegate.parseCustomElement(root);
        }
    }
      //其他方法。。。。
}

上面的代码逻辑还是很清晰的,一种处理默认标签,如

<bean "id=test" class="com.Test">

另一类是自定义的。如下

<tx:annotation-driven/>

下面我们来看一下自定义标签的解析原理

public class BeanDefinitionParserDelegate {
    public BeanDefinition parseCustomElement(Element ele) {
        return parseCustomElement(ele, null);
    }

    public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
        String namespaceUri = getNamespaceURI(ele);
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }
}

我们看到,其实自定义标签的处理流程很简单。

  • 1.获取到命名空间
  • 2.根据命名空间找到对应的命名空间处理器
  • 3.根据用户自定义的处理器进行解析

第一步很简单,我们跳过。我们来看一下第二步的实现。

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
    /**
     * Locate the {@link NamespaceHandler} for the supplied namespace URI
     * from the configured mappings.
     * @param namespaceUri the relevant namespace URI
     * @return the located {@link NamespaceHandler}, or {@code null} if none found
     */
    @Override
    public NamespaceHandler resolve(String namespaceUri) {
        // 获取所有已经配置的handler映射
        Map<String, Object> handlerMappings = getHandlerMappings();
        // 根据命名空间获取对应的信息
        Object handlerOrClassName = handlerMappings.get(namespaceUri);
        if (handlerOrClassName == null) {
            return null;
        }
        else if (handlerOrClassName instanceof NamespaceHandler) {
            // 已经做过解析的直接从缓存中获取
            return (NamespaceHandler) handlerOrClassName;
        }
        else {
            // 没有做过解析的,则返回的是类路径
            String className = (String) handlerOrClassName;
            try {
                // 使用反射将类路径转化为类
                Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
                if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                    throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                            "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
                }
                // 初始化处理器
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
                // 调用自定义的NameSpaceHandler的init方法
                namespaceHandler.init();
                // 存入缓存
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
            catch (ClassNotFoundException ex) {
                throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
                        namespaceUri + "] not found", ex);
            }
            catch (LinkageError err) {
                throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
                        namespaceUri + "]: problem with handler class file or dependent class", err);
            }
        }
    }

        /**
     * Load the specified NamespaceHandler mappings lazily.
     */
    private Map<String, Object> getHandlerMappings() {
        // 如果没有缓存则开始进行缓存
        if (this.handlerMappings == null) {
            synchronized (this) {
                if (this.handlerMappings == null) {
                    try {
                        // this.handlerMappingsLocation在构造函数中初始化为META-INF/spring.handlers
                        Properties mappings =
                                PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                        if (logger.isDebugEnabled()) {
                            logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                        }
                        Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
                        CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                        this.handlerMappings = handlerMappings;
                    }
                    catch (IOException ex) {
                        throw new IllegalStateException(
                                "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                    }
                }
            }
        }
        return this.handlerMappings;
    }
}

现在我们知道了为什么需要在META-INF/spring.handlers文件中那样配置了。因为在加载所有的处理器的配置的默认目录就是META-INF/spring.handlers

当获取到自定义的NameSpaceHandler之后就可以进行处理器初始化并解析了。实际上就是调用init方法。我们之前使用自定义NameSpaceHandler的时候就用到了。在init方法中注入解析器如下

public class AopNamespaceHandler extends NamespaceHandlerSupport {

    /**
     * Register the {@link BeanDefinitionParser BeanDefinitionParsers} for the
     * '{@code config}', '{@code spring-configured}', '{@code aspectj-autoproxy}'
     * and '{@code scoped-proxy}' tags.
     */
    @Override
    public void init() {
        // In 2.0 XSD as well as in 2.1 XSD.
        registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
    }

}

到现在为止,我们算是真正知道了怎么自定义新的XML标签,并且实现一个自定义的NameSpaceHandler。而且我么也知道了为什么要这么做。所以总结起来就是一句话

源代码面前了无秘密

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,394评论 25 707
  • 聚宝盆内金钱树 牡丹墙下伪君子 花非花 树非树 人物非人物 借用富庶物 怎得真富足 如若无实才 唯有一个俗
    康真阅读 209评论 0 0
  • 【嗨新新新20171101设计改变一切 学而思d20】 今天老师讲关于乐观情绪的延伸说到“心情处理好,事情会更好”...
    嗨新新新阅读 188评论 0 0