写点什么

看了小姐姐的 Spring SPI 总结,双非渣硕的我差点跳起来,被征服了

发布于: 2021 年 05 月 07 日
看了小姐姐的Spring SPI 总结,双非渣硕的我差点跳起来,被征服了

上一篇是分享的是《JVM 中的 Java 对象》,这篇分享的是《Spring SPI 机制总结》。


Spring SPI 机制总结

1、概念:

SPI(Service Provider Interface)服务提供接口,简单来说就是用来解耦,实现插件的自由插拔,具体实现方案可参考 JDK 里的 ServiceLoader(加载 classpath 下所有 META-INF/services/目录下的对应给定接口包路径的文件,然后通过反射实例化配置的所有实现类,以此将接口定义和逻辑实现分离)Spring 在 3.0.x 的时候就已经引入了 spring.handlers,很多博客讲 Spring SPI 的时候并没有提到 spring.handlers,但是通过我自己的分析对比,其实 spring.handlers 也是一种 SPI 的实现,只不过它是基于 xml 的,而且在没有 boot 的年代,它几乎是所有三方框架跟 spring 整合的必选机制。

在 3.2.x 又新引入了 spring.factories,它的实现跟 JDK 的 SPI 就基本是相似的了。

spring.handlers 和 spring.factories 我都把它归纳为 Spring 为我们提供的 SPI 机制,通过这两种机制,我们可以在不修改 Spring 源码的前提下,非常轻松的做到对 Spring 框架的扩展开发。

2、实现:

2.1 先看看 spring.handlers SPI

在 Spring 里有个接口 NamespaceHandlerResolver,只有一个默认的实现类 DefaultNamespaceHandlerResolver,而它的作用就是加载 classpath 下可能分散在各个 jar 包中的 META-INF/spring.handlers 文件,resolve 方法中关键代码如下:

//加载所有jar包中的META-INF/spring.handlers文件Properties mappings=PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
//把META-INF/spring.handlers中配置的namespaceUri对应实现类实例化NamespaceHandler namespaceHandler =(NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
复制代码

DefaultNamespaceHandlerResolver.resolve()主要被用在 BeanDefinitionParserDelegate 的 parseCustomElement 和 decorateIfRequired,所以 spring.handlers SPI 机制主要也是被用在 bean 的扫描和解析过程中。

2.2 再来看 spring.factories SPI

// 获取某个已定义接口的实现类,跟JDK的ServiceLoader SPI相似度为90%List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(BeanInfoFactory.class, classLoader);

复制代码


// spring.factories文件的格式为:key=value1,value2,value3// 从所有jar文件中找到MET-INF/spring.factories文件(注意是:classpath下的所有jar包,所以可插拔、扩展性超强)Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :	ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));List<String> result = new ArrayList<String>();while (urls.hasMoreElements()) {	URL url = urls.nextElement();	Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));	String propertyValue = properties.getProperty(factoryClassName);	for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {		result.add(factoryName.trim());	}}return result;
复制代码

更多细节,大家可以参考 SpringFactoriesLoader 类,Spring 自 3.2.x 引入 spring.factories SPI 后其实一直没怎么利用起来,只有 CachedIntrospectionResults(初始化 bean 的过程中)用到了,而且在几大核心 jar 包里,也只有 bean 包里才有用到。真正把 spring.factories 发扬光大的,是到了 Spring Boot,可以看到 boot 包里配置了非常多的接口实现类。大家跟踪 boot 的启动类 SpringApplication 可以发现,有很多地方都调用了 getSpringFactoriesInstances()方法,这些就是 spring boot 开给我们的扩展机会,就像一座宝藏一样,大家可以自己去发掘。


3、应用:

先来看看 mybatis 和 dubbo 早期跟 Spring 整合的实现,他们无一例外都用到了 spring.handlers SPI 机制,以此来向 IOC 容器注入自己的 Bean。


进入 boot 时代后,spring.factories SPI 机制应用得更加广泛,我们可以在容器启动、环境准备、初始化、上下文加载等等环节轻轻松松的对 Spring 做扩展开发(例如:我们项目中用到 spring.factories SPI 机制对配置文件中的变量实现动态解密,以及前篇博文中提到的 @Replace 注解等)。

4、实践(加载 application.xyz 配置文件):

Spring 里有两种常见的配置文件类型:application.properties 和 application.yml,其中 yml 是近年兴起的,但说实话同事也包括我自己是被它坑过,没有合适的编辑器时很容易把格式写错,导致上线出问题。所以我就在想有没有办法让 Spring 支持一种新的配置文件格式,既保留 yml 的简洁优雅,又能够有强制的格式校验,暂时我想到了 json 格式。


# 这是spring.factories中的配置org.springframework.boot.env.PropertySourceLoader=top.hiccup.json.MyJsonPropertySourceLoader
复制代码


public class MyJsonPropertySourceLoader implements PropertySourceLoader {    @Override    public String[] getFileExtensions() {        return new String[]{"xyz"};    }    @Override    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream())); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } // 这里只是做了简单解析,没有做嵌套配置的解析 JSONObject json = JSONObject.parseObject(sb.toString()); List<PropertySource<?>> propertySources = new ArrayList<>(); MapPropertySource mapPropertySource = new MapPropertySource(resource.getFilename(), json); propertySources.add(mapPropertySource); return propertySources; }}
复制代码


ConfigurableApplicationContext ctx = SpringApplication.run(BootTest.class, args);        Custom custom = ctx.getBean(Custom.class);        System.out.println(custom.name);        System.out.println(custom.age);
复制代码

运行得到结果如下:


可见我们在不修改 Spring 源码的前提下,轻松通过 Spring 开放给我们的扩展性实现了对新的配置文件类型的加载和解析。

这就是 Spring SPI 的魅力吧。


  • 以上就是《Spring SPI 机制总结》的分享。

  • 也欢迎大家交流探讨,该文章若有不正确的地方,希望大家多多包涵。

  • 你们的支持就是我最大的动力,如果对大家有帮忙给个赞哦~~~

用户头像

还未添加个人签名 2021.04.26 加入

还未添加个人简介

评论

发布
暂无评论
看了小姐姐的Spring SPI 总结,双非渣硕的我差点跳起来,被征服了