Java Spring

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整合的必选机制。
2021-07-25-19-16-58-184731.png
在3.2.x又新引入了spring.factories,它的实现跟JDK的SPI就基本是相似的了。
2021-07-25-19-16-58-295757.jpeg
spring.handlersspring.factories都可以归纳为Spring提供的SPI机制,通过这两种机制,可以在不修改Spring源码的前提下,非常轻松的做到对Spring框架的扩展开发。

2、实现

2.1 先看看spring.handlers SPI

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

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

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

2.2 再来看spring.factories SPI

  1. // 获取某个已定义接口的实现类,跟JDK的ServiceLoader SPI相似度为90%
  2. List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(BeanInfoFactory.class, classLoader);
  3. // spring.factories文件的格式为:key=value1,value2,value3
  4. // 从所有jar文件中找到MET-INF/spring.factories文件(注意是:classpath下的所有jar包,所以可插拔、扩展性超强)
  5. Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
  6. ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
  7. List<String> result = new ArrayList<String>();
  8. while (urls.hasMoreElements()) {
  9. URL url = urls.nextElement();
  10. Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
  11. String propertyValue = properties.getProperty(factoryClassName);
  12. for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
  13. result.add(factoryName.trim());
  14. }
  15. }
  16. return result;

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

3、应用

先来看看Mybatis和Dubbo早期跟Spring整合的实现,他们无一例外都用到了spring.handlers SPI机制,以此来向IOC容器注入自己的Bean。
2021-07-25-19-16-58-703729.png2021-07-25-19-16-58-840729.png
进入boot时代后,spring.factories SPI机制应用得更加广泛,可以在容器启动、环境准备、初始化、上下文加载等等环节轻轻松松的对Spring做扩展开发(例如:项目中用到spring.factories SPI机制对配置文件中的变量实现动态解密)。

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

Spring里有两种常见的配置文件类型:application.properties 和 application.yml,其中yml是近年兴起的,但是没有合适的编辑器时很容易把格式写错,导致上线出问题。所以有没有办法让Spring支持一种新的配置文件格式,既保留yml的简洁优雅,有能够有强制的格式校验,暂时想到了json格式。
2021-07-25-19-16-59-044731.png

  1. # 这是spring.factories中的配置
  2. org.springframework.boot.env.PropertySourceLoader=top.hiccup.json.MyJsonPropertySourceLoader
  1. public class MyJsonPropertySourceLoader implements PropertySourceLoader {
  2. @Override
  3. public String[] getFileExtensions() {
  4. return new String[]{"xyz"};
  5. }
  6. @Override
  7. public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
  8. BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));
  9. StringBuilder sb = new StringBuilder();
  10. String line;
  11. while ((line = reader.readLine()) != null) {
  12. sb.append(line);
  13. }
  14. // 这里只是做了简单解析,没有做嵌套配置的解析
  15. JSONObject json = JSONObject.parseObject(sb.toString());
  16. List<PropertySource<?>> propertySources = new ArrayList<>();
  17. MapPropertySource mapPropertySource = new MapPropertySource(resource.getFilename(), json);
  18. propertySources.add(mapPropertySource);
  19. return propertySources;
  20. }
  21. }
  22. ConfigurableApplicationContext ctx = SpringApplication.run(BootTest.class, args);
  23. Custom custom = ctx.getBean(Custom.class);
  24. System.out.println(custom.name);
  25. System.out.println(custom.age);

具体代码可以参考(https://github.com/hiccup234/web-advanced/tree/master/configFile) ,运行得到结果如下:
2021-07-25-19-16-59-141733.png
可见在不修改Spring源码的前提下,轻松通过Spring开放的扩展性实现了对新的配置文件类型的加载和解析。
这就是Spring SPI的魅力吧。