背景:在Hibernate validator learning的参考链接文章中提到了SPI,用于资源发现,动态代码。这里系统地学习下SPI机制和其常用场景。
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,例如:java.sql.Driver接口。Java的SPI机制可以为某个接口寻找服务实现。
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务实现的工具类是:java.util.ServiceLoader。
SPI的创建过程为:
接口->实现并在resource下的META-INF/services创建一个名为前面接口全名的文件,文件内容为实现类的全名,打包为jar包->在需要用到该接口的地方,将前面jar包放在类路径,ServiceLoader.load(接口名)返回ServiceLoader<接口类型>,该返回值可能有多个service provider,StreamSupport.stream(ServiceLoader<接口类型>.spliterator(),false).findFirst(),false表示不创建并行流。这样操作后返回Optional<接口类型>,通过get()返回接口实现类的对象。或通过orElse(new xxx)返回默认接口实现类的对象。
数据库DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI机制。比较感兴趣后两者中的SPI主要做了哪些事。
而JDBC4.0之后不需要Class.forName
来加载驱动,直接获取连接即可,这里使用了Java的SPI扩展机制来实现。
ServiceLoader的spliterator()和iterator()的区别是前者继承于java.lang.Iterable,后者lazily load providers,在hasNext()和next()中parse provider configuration and class来实现懒加载以及当出现不合要求的provider时抛出java.util.ServiceConfigurationError类型错误。
以上参考附录中的第一条链接再加上自己的理解。
第二条链接有补充SPI的实现类必须携带一个不带参数的构造方法。以及在已有框架中的使用,
- 日志门面接口实现类加载 SLF4J加载不同提供商的日志实现类
- Spring Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
第三条链接讲到一些框架中使用SPI的场景
eclipse插件:
最具spi思想的应该属于插件开发。Eclipse使用OSGi作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。
一般来说,插件的文件结构必须在指定目录下包含以下三个文件:
- META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
- build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
- plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等
当eclipse启动时,会遍历plugins文件夹中的目录,扫描每个插件的清单文件MANIFEST.MF,并建立一个内部模型来记录它所找到的每个插件的信息,就实现了动态添加新的插件。
这也意味着是eclipse制定了一系列的规则,像是文件结构、类型、参数等。插件开发者遵循这些规则去开发自己的插件,eclipse并不需要知道插件具体是怎样开发的,只需要在启动的时候根据配置文件解析、加载到系统里就好了,是spi思想的一种体现。
Spring
Spring中运用到spi思想的地方也有很多,下面随便列举几个。
scan: 在spring中可以通过component-scan标签来对指定包路径进行扫描,只要扫到spring制定的@service、@controller等注解,spring自动会把它注入容器。这就相当于spring制定了注解规范,我们按照这个注解规范开发相应的实现类或controller,spring并不需要感知我们是怎么实现的,他只需要根据注解规范和scan标签注入相应的bean,这正是spi理念的体现。
scope: 像是自定义一个 ThreadScope实现Scope接口,再把它注册到beanFactory中,接着就能在xml中使用它了。这也是平台使用方制定规则,提供方负责实现的思想。
自定义标签:
扩展Spring自定义标签配置大致需要以下几个步骤
- 创建一个需要扩展的组件,也就是一个bean
- 定义一个XSD文件描述组件内容,也可以给bean的属性赋值啥的
- 创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和对组件进行初始化,像是为组件bean赋上xsd里设置的值
- 创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器,重写其中的的init方法
这样我们就边写出了一个自定义的标签,spring只是为我们定义好了创建标签的流程,不用感知我们是如何实现的,我们通过register就把自定义标签加载到了spring中,实现了spi的思想。
ConfigurableBeanFactory:
spring里为我们提供了许多属性编辑器,这时我们如果想把spring配置文件中的字符串转换成相应的对象进行注入,就要自定义属性编辑器,这时我们可以按照spring为我们提供的规则来自定义我们的编辑器
自定义好了属性编辑器后,ConfigurableBeanFactory里面有一个registerCustomEditor方法,此方法的作用就是注册自定义的编辑器,也是spi思想的体现
hotspot:
不同的厂商会提供hotspot的不同实现,在hotspot启动的时候,会判断当前是什么系统来启动不同的实现,这也是一种spi的思想。
还有像是Jetty/Tomcat中自定义sessionManager、自定义线程池,dubbo的扩展机制等内容也属于spi。其实在这里就可以发现,只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想。
参考链接:
https://juejin.im/post/5af952fdf265da0b9e652de3
https://juejin.im/post/5b9b1c115188255c5e66d18c
https://zhuanlan.zhihu.com/p/28909673