前语:不要为了读文章而读文章,一定要带着问题来读文章,勤思考。

作者:宋顺   来源:nobodyiam.com

# 背景介绍

Spring现在几乎已经成为了Java开发的必备框架,在享受Spring框架本身强大能力的同时,有时我们也会希望自己研发的组件和Spring进行整合,从而使得组件更易于上手,而且配合Spring使用能发挥更强大的作用。

Apollo配置中心的Java客户端在前一段时间也提供了和Spring整合的功能,Apollo既支持传统的基于XML的配置,也支持目前比较流行的基于Java的配置。下面就以Apollo为例,简单介绍一下扩展Spring的几种方式。

# 基于XML配置的扩展

相信从事Java开发有一些年头的人一定会对Spring的xml配置方式非常熟悉。不管是bean的定义,还是Spring自身的配置,早期都是通过xml配置完成的。相信还是有一大批遗留项目目前还是基于xml配置的,所以支持xml的配置方式是一个必选项。

1、定义schema

要支持XML的配置方式,首先需要定义一套XML Schema来描述组件所提供的功能。

Apollo提供了向Spring Property Sources注入配置的功能,所以schema中就需要描述我们期望用户提供的namespace以及namespace之间的排序等元数据。

下面就是Apollo的schema示例,可以看到xml的配置节点名字是config,并且有两个可选属性:namespaces和order,类型分别是string和int。

<?xml version="1.0" encoding="UTF-8" standalone="no"?><xsd:schema xmlns="http://www.ctrip.com/schema/apollo"            xmlns:xsd="http://www.w3.org/2001/XMLSchema"            targetNamespace="http://www.ctrip.com/schema/apollo"            elementFormDefault="qualified"            attributeFormDefault="unqualified">    <xsd:annotation>        <xsd:documentation>xsd:documentation>    xsd:annotation>    <xsd:element name="config">        <xsd:annotation>            <xsd:documentation>                            xsd:documentation>        xsd:annotation>        <xsd:complexType>            <xsd:attribute name="namespaces" type="xsd:string" use="optional">                <xsd:annotation>                    <xsd:documentation>                                                    The comma-separated list of namespace names to integrate with Spring property sources.                            If not specified, then default to application namespace.                        ]]>                    xsd:documentation>                xsd:annotation>            xsd:attribute>            <xsd:attribute name="order" type="xsd:int" use="optional">                <xsd:annotation>                    <xsd:documentation>                                                    The order of the config, default to Ordered.LOWEST_PRECEDENCE, which is Integer.MAX_VALUE.                            If there are properties with the same name in different apollo configs, the config with smaller order wins.                        ]]>                    xsd:documentation>                xsd:annotation>            xsd:attribute>        xsd:complexType>    xsd:element>xsd:schema>

2、创建NamespaceHandler

除了XML Schema,我们还需要创建一个自定义的NamespaceHandler来负责解析用户在XML中的配置。

继承NamespaceHandlerSupport

为了简化代码,我们一般会继承一个helper类:NamespaceHandlerSupport,然后在init方法中注册处理我们自定义节点的BeanDefinitionParser。

下面的示例告诉Spring由我们自定义的的BeanParser来处理xml中的config节点信息。

public class NamespaceHandler extends NamespaceHandlerSupport {  @Override  public void init() {    registerBeanDefinitionParser("config", new BeanParser());  }}

自定义BeanDefinitionParser

自定义的BeanDefinitionParser负责解析xml中的config节点信息,记录用户的配置信息,为后面和Spring整合做好铺垫。

Apollo的自定义BeanDefinitionParser主要做了两件事情:

  • 记录用户配置的namespace和order

  • 向Spring注册Bean:ConfigPropertySourcesProcessor,这个bean后面会实际处理用户配置的namespace和order,从而完成配置注入到Spring中的功能

public class BeanParser extends AbstractSingleBeanDefinitionParser {  @Override  protected Class> getBeanClass(Element element) {    return ConfigPropertySourcesProcessor.class;  }  @Override  protected boolean shouldGenerateId() {    return true;  }  @Override  protected void doParse(Element element, BeanDefinitionBuilder builder) {    String namespaces = element.getAttribute("namespaces");    //default to application    if (Strings.isNullOrEmpty(namespaces)) {      namespaces = ConfigConsts.NAMESPACE_APPLICATION;    }    int order = Ordered.LOWEST_PRECEDENCE;    String orderAttribute = element.getAttribute("order");    if (!Strings.isNullOrEmpty(orderAttribute)) {      try {        order = Integer.parseInt(orderAttribute);      } catch (Throwable ex) {        throw new IllegalArgumentException(          String.format("Invalid order: %s for namespaces: %s", orderAttribute, namespaces));      }    }    PropertySourcesProcessor.addNamespaces(NAMESPACE_SPLITTER.splitToList(namespaces), order);  }}

3、注册Spring handler和Spring schema

基于XML配置扩展Spring的主体代码基本就是上面这些,剩下的就是要让Spring解析xml配置文件的过程中识别我们的自定义节点,并且转交到我们的NamespaceHandler处理。

META-INF/spring.handlers

首先需要在META-INF目录下创建一个spring.handlers文件,来配置我们自定义的XML Schema Namespace到我们自定义的NamespaceHandler映射关系。

http\://www.ctrip.com/schema/apollo=com.ctrip.framework.apollo.spring.config.NamespaceHandler

META-INF/spring.schemas

我们还需要在META-INF目录下创建一个spring.schemas,来配置我们自定义的XML Schema地址到实际Jar包中的classpath映射关系(避免Spring真的去服务器上下载不存在的文件)。

为了简单起见,Apollo把实际的schema文件放在了META-INF目录下。

http\://www.ctrip.com/schema/apollo-1.0.0.xsd=/META-INF/apollo-1.0.0.xsdhttp\://www.ctrip.com/schema/apollo.xsd=/META-INF/apollo-1.0.0.xsd

4、样例目录结构

按照上面的方式,最终Apollo和Spring整合的相关代码结构如下图所示:

5、使用样例

基于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"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">        <apollo:config namespaces="application" order="1"/>    beans>

# 基于Java配置的扩展

从Spring 3.0开始,一种新的基于Java的配置方式出现了。

通过这种方式,我们在开发Spring项目的过程中再也不需要去配置繁琐的xml文件了,只需要在Configuration类中配置就可以了,大大的简化了Spring的使用。

另外,这也是Spring Boot默认的配置方式,所以建议也支持这一特性。

1、@Import注解

支持Java配置扩展的关键点就是@Import注解,Spring 3.0提供了这个注解用来支持在Configuration类中引入其它的配置类,包括Configuration类, ImportSelector和ImportBeanDefinitionRegistrar的实现类。

我们可以通过这个注解来引入自定义的扩展Bean。

2、自定义注解

和基于XML配置类似的,我们需要提供给用户一个注解来配置需要注入到Spring Property Sources的namespaces和order。

下面就是Apollo提供的@EnableApolloConfig注解,允许用户传入namespaces和order信息。

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(ApolloConfigRegistrar.class)public @interface EnableApolloConfig {  /**   * Apollo namespaces to inject configuration into Spring Property Sources.   */  String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};  /**   * The order of the apollo config, default is {@link Ordered#LOWEST_PRECEDENCE}, which is Integer.MAX_VALUE.   * If there are properties with the same name in different apollo configs, the apollo config with smaller order wins.   */  int order() default Ordered.LOWEST_PRECEDENCE;}

这里的关键点是在注解上使用了@Import(ApolloConfigRegistrar.class),从而Spring在处理@EnableApolloConfig时会实例化并调用ApolloConfigRegistrar的方法。

3、自定义ImportBeanDefinitionRegistrar实现

ImportBeanDefinitionRegistrar接口定义了registerBeanDefinitions方法,从而允许我们向Spring注册必要的Bean。

Apollo的自定义ImportBeanDefinitionRegistrar实现(ApolloConfigRegistrar)主要做了两件事情:

  • 记录用户配置的namespace和order

  • 向Spring注册Bean:PropertySourcesProcessor,这个bean后面会实际处理用户配置的namespace和order,从而完成配置注入到Spring中的功能

public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {  @Override  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {    AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata        .getAnnotationAttributes(EnableApolloConfig.class.getName()));    String[] namespaces = attributes.getStringArray("value");    int order = attributes.getNumber("order");    PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order);    BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(),        PropertySourcesProcessor.class);  }}

4、样例目录结构

按照上面的方式,最终Apollo和Spring整合的相关代码结构如下图所示:

5、使用样例

基于Java配置的使用样例如下:

@Configuration@EnableApolloConfig(value = "application", order = 1)public class AppConfig {}

# Spring容器的扩展点

前面两节简单介绍了扩展Spring的两种方式:基于XML和基于Java的配置。通过这两种方式,我们可以在运行时收集到用户的配置信息,同时向Spring注册实际处理这些配置信息的Bean。

但这些注册进去的Bean实际上是如何工作的呢?我们通过什么方式能使我们的程序逻辑和Spring的容器紧密合作,并无缝插入到用户bean的生命周期中呢?

这里简单介绍Spring容器最常用的两个扩展点:BeanFactoryPostProcessor和BeanPostProcessor。

1、BeanFactoryPostProcessor

BeanFactoryPostProcessor提供了一个方法:postProcessBeanFactory。

这个方法会被Spring在容器初始化过程中调用,调用时机是所有bean的定义信息都已经初始化好,但是这些bean还没有实例化。

Apollo就利用这个时间点把配置信息注入到Spring Property Sources中,从而用户的bean在真正实例化时,所有需要的配置信息已经准备好了。

public class PropertySourcesProcessor implements BeanFactoryPostProcessor {  private static final AtomicBoolean PROPERTY_SOURCES_INITIALIZED = new AtomicBoolean(false);  @Override  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {    if (!PROPERTY_SOURCES_INITIALIZED.compareAndSet(false, true)) {      //already initialized      return;    }    //initialize and inject Apollo config to Spring Property Sources    initializePropertySources();  }}

2、BeanPostProcessor

BeanPostProcessor提供了两个方法:postProcessBeforeInitialization和postProcessAfterInitialization,主要针对bean初始化提供扩展。

  • postProcessBeforeInitialization会在每一个bean实例化之后、初始化(如afterPropertiesSet方法)之前被调用。

  • postProcessAfterInitialization则在每一个bean初始化之后被调用。

我们常用的@Autowired注解就是通过postProcessBeforeInitialization实现的(AutowiredAnnotationBeanPostProcessor)。

Apollo提供了@ApolloConfig注解来实现实例化时注入Config对象实例,所以实现逻辑和@Autowired类似。

public class ApolloAnnotationProcessor implements BeanPostProcessor {  @Override  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {    Class clazz = bean.getClass();    processFields(bean, clazz.getDeclaredFields());    return bean;  }  @Override  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {    return bean;  }  private void processFields(Object bean, Field[] declaredFields) {    for (Field field : declaredFields) {      ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);      if (annotation == null) {        continue;      }      Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),          "Invalid type: %s for field: %s, should be Config", field.getType(), field);      String namespace = annotation.value();      Config config = ConfigService.getConfig(namespace);      ReflectionUtils.makeAccessible(field);      ReflectionUtils.setField(field, bean, config);    }  }}

仔细阅读上面的代码就会发现Apollo在用户bean初始化前会根据@ApolloConfig的配置注入对应namespace的Config实例。

# 总结

本文简单介绍了扩展Spring的几种方式,下面简单小结一下,希望对大家有所帮助。

通过基于XML和基于Java的配置扩展,可以使用户通过Spring使用我们研发的组件,提供很好的易用性。

通过Spring容器最常用的两个扩展点:BeanFactoryPostProcessor和BeanPostProcessor,可以使我们的程序逻辑和Spring容器紧密合作,无缝插入到用户bean的生命周期中,发挥更强大的作用。

热文推荐

面试官:请从源码的角度,来说说Hashtable与HashMap的区别?

一篇讲透全网最高频的Java NIO面试考点汇总。

同事问我,为什么阿里P3C不建议返回值用枚举,而参数可以呢?

同时,分享一份Java面试资料给大家,覆盖了算法题目、常见面试题、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 面试 领取。

写一个带输入输出的存储过程_携程大佬带你写一个可扩展的Spring插件。相关推荐

  1. oracle接收输入参数,Oracle带输入输出参数存储过程(包括sql分页功能)

    记录一下,免得以后忘记了又要到处去找. begin /*这里不能直接执行select语句但可以直接执行update.delete.insert语句*/ end里面不能接执行select语句,声明会话级 ...

  2. 什么是容器服务_携程万台规模容器云平台运维管理实践

    *本文来自于周昕毅在GOPS全球运维大会上的分享,由高效运维公众号整理,略有修改* 前言 本文将分享携程在私有云平台管理实践过程中踩过的坑和遇到的问题,包含: 第一部分,携程容器云概览 第二部分,容器 ...

  3. element js 包含字符_携程春招题目字符串截取和数组升维

    编程风格 简单陈述一下文中代码使用的编程风格: 使用 ES5,以避免有些在线编程平台不支持 ES6 的状况(所以在这里没有用 new Set()) Airbnb 代码规范,不使用单 var 模式 变量 ...

  4. tp5.1 获取表里的所有数据_携程机票数据仓库建设之路

    一.前言 随着大数据技术的飞速发展,海量数据存储和计算的解决方案层出不穷,生产环境和大数据环境的交互日益密切.数据仓库作为海量数据落地和扭转的重要载体,承担着数据从生产环境到大数据环境.经由大数据环境 ...

  5. python携程酒店评论_携程酒店评论爬虫心得

    携程酒店评论爬虫心得 发布时间:2018-09-02 15:58, 浏览次数:613 携程酒店评论爬虫心得 这次爬取数据,遇到了不少的困难,也走了很多弯路,特此写下帮助记忆.以下基本是我爬取数据的过程 ...

  6. clickhouse集群表删除_携程用ClickHouse轻松玩转每天十亿级数据更新

    作者介绍 蔡岳毅,携程酒店大数据高级研发经理,负责酒店数据智能平台研发,大数据技术创新工作.喜欢探索研究大数据的开源技术框架. 一.背景 携程酒店每天有上千表,累计十多亿数据更新,如何保证数据更新过程 ...

  7. 订单失效怎么做的_携程技术专家:数据库压力降低90%,订单缓存系统架构实践...

    来源:携程技术(ID:ctriptech) 本文旨在分享携程机票后服务订单处理团队,在构建机票订单缓存系统过程中的一些思考总结,希望能给大家一些启发或帮助.通篇分为以下七大部分:背景,瓶颈,选型,架构 ...

  8. 二阶差分预测后数据还原公式_携程如何基于ARIMA时序分析做业务量的预测

    一. 前言 时间序列分析是统计学科的一个重要分支.它主要是通过研究随着时间的推移事物发展变化过程中的规律,来进行事物未来发展情况的预测.在我们的日常生活中,股票的价格走势,奶茶店每天的销售额,一年的降 ...

  9. java 修改最大nio连接数_携程基于Quasar协程的NIO实践

    IO密集型系统在高并发场景下,会有大量线程处于阻塞状态,性能低下,JAVA上成熟的非阻塞IO(NIO)技术可解决该问题.目前Java项目对接NIO的方式主要依靠回调,代码复杂度高,降低了代码可读性与可 ...

最新文章

  1. golang中的bufio
  2. 基于select模型的TCP服务器
  3. 深度学习:人脸识别学习笔记
  4. python删除为空值的列_python如何删除列为空的行
  5. springboot接口入参下划线转驼峰以及返回参数驼峰转下划线实现
  6. tiny4412--linux驱动学习(2)
  7. 基于深度学习的文本分类2
  8. apipost使用mock随机获取多组数据中的一组数据进行测试
  9. python数据结构剑指offer-两个链表的第一个公共结点
  10. 已解决:Cannot find ./catalina.sh The file is absent or does not have execute permission This file is ne
  11. Spring 最常用的 7 大类注解,史上最强整理!
  12. 2.卷2(进程间通信)---Posix IPC
  13. java深拷贝和浅拷贝
  14. 峰值,峰峰值,有效值的基本知识
  15. 【Scrum】借由数个冲刺,实现产品的敏捷开发!
  16. qq音乐排行榜数据爬取
  17. 记一次quartz定时任务不执行排雷
  18. 为什么Elasticsearch查询变得这么慢了?
  19. 受用一生的高效PyCharm使用技巧(三)
  20. 微信小程序 获取当前日期时间

热门文章

  1. 简化前端开发的javascript工具类收集
  2. oracle改表结构非空字段类型,oracle 表结构的非完全复制
  3. 2016二级c语言成绩查询,2016年12月计算机二级C语言测试及答案
  4. es6 混合commjs_前端模块化——CommonJS、ES6规范
  5. mysql安装教程_mysql8.0.20安装教程,mysql下载安装教程8.0.20
  6. 基于matlab的车牌识别系统设计错误,基于MATLAB的车牌识别系统设计
  7. Python使用property函数和使用@property装饰器定义属性访问方法的异同点分析
  8. oracle函数 power(x,y)
  9. Codeforces.1051G.Distinctification(线段树合并 并查集)
  10. 404 Note Found 队 Alpha8