jdk的SPI机制
spi简介
模块之间一般推荐基于接口编程,不与具体的实现类耦合。抽象接口可能有多种不同的实现方案,eg. 日志可以使用不同的日志框架来实现,json|xml|excel可以使用不同的组件框架来解析,jdbc可以使用mysql、oracle等不同的数据库驱动实现,等等。
SPI全称Service Provider Interface,是jdk提供的一套服务发现机制,在开源框架、组件中十分常用。
spi常用场景
- 组件替换机制,插拔式组件设计,eg. jdbc的数据库驱动、日志实现框架
- 自定义扩展,eg. spring的一些接口可以通过spi来实现自定义扩展
双击shift搜索 META-INF/services,可以看到 jdk、jackson、junit、spring、日志组件、数据库驱动都使用到了jdk的spi机制。
SPI 使用约定
在提供者所在模块的 resources 下新建目录 META-INF/services,META-INF/services 目录下新建文件,以服务接口的全限定接口名作为文件名,文件中指定该服务接口要使用的具体实现类。
服务提供者即服务接口的实现者,服务接口模块只提供spi接口定义,服务提供者模块提供接口实现,并在 resources 下的 META-INF/services 中新建服务接口对应的配置文件,指定自身提供的接口实现类,来把自身提供的服务接口实现暴露出去。
spi实际是“ 基于接口编程+策略模式+配置文件 ”组合实现的动态加载机制,服务提供者通过本地配置文件的方式注册具体的服务接口实现类,服务接口模块通过对应的配置文件发现、获取到对应的接口实现类。
spi将服务接口与服务实现分离,解耦,将装配的控制权移到服务提供者本身,插拔式设计,提升了扩展性,在模块化设计中十分常用。
使用示例
1、使用者模块中定义的接口 AnimalServic
public interface AnimalService {
void say();
}
2、提供者模块中提供的实现类 DogService、CatService
public class DogService implements AnimalService {
@Override
public void say() {
System.out.println("汪汪汪");
}
}
public class CatService implements AnimalService {
@Override
public void say() {
System.out.println("喵喵喵");
}
}
3、在提供者模块的 resources下新建目录 META-INF/services,services 目录下新建文件 com.chy.mall.service.AnimalService,文件名是接口的全限定名称,指定当前模块给该接口提供的实现
#全限定类名,通常只有一个类,也可以指定多个实现类,指定多个时一行一个
com.chy.demo.service.impl.DogService
com.chy.demo.service.impl.CatService
4、使用者模块中的使用示例
// ServiceLoader#load 只是创建 ServiceLoader 实例,做加载的准备工作,尚未解析配置文件
ServiceLoader<AnimalService> serviceLoader = ServiceLoader.load(AnimalService.class);
//遍历指定接口的实现类。临时变量需要声明为接口类型
for (AnimalService animalService : serviceLoader) {
//执行实现的方法
animalService.say();
}
jdk的spi机制提供了 ServiceLoader 类用于加载、解析spi接口的配置文件,ServiceLoader实现了Iterable接口,可迭代,但只能以迭代器的方式进行操作。
使用 hasNext() 时才会加载解析 META-INF/services 下对应的接口配置文件,使用 next() 时才会通过实现类的 Class 对象的 newInstance() 方法(实质是通过反射调用无参构造器)创建实例。
spring的factories机制
factories机制简介
factories可以看做是spring结合自身需要提供的一种spi机制,设计思想和jdk的spi机制差不多。区别:jdk的spi机制,一个配置文件只能指定一个接口要使用的实现类;spring的factories机制,一个配置文件可以指定多个接口要使用的实现类。
factories机制在spring家族中广泛使用,双击shift搜索 spring.factories,可以看到spring、springboot、springcloud中都大量使用了factories机制。spring体系的很多扩展配置都是通过 spring.factories 指定的,比如应用初始化器 ApplicationContextInitializer、应用监听器 ApplicationListener。
注解本质是一种特殊接口,也可以用 factories 指定实现类,比如springboot中的 @EnableAutoConfiguration 注解就使用了factories指定要应用的实现类。
使用示例
1、使用者模块中定义的接口 AnimalService
public interface AnimalService {
void say();
}
2、提供者模块中提供的实现类 DogService、CatService
public class DogService implements AnimalService {
@Override
public void say() {
System.out.println("汪汪汪");
}
}
public class CatService implements AnimalService {
@Override
public void say() {
System.out.println("喵喵喵");
}
}
3、在提供者模块的 resources下新建文件 META-INF/spring.factories,指定当前模块提供的接口实现
#spring.factories本质是一个properties文件,以键值对的形式写配置
#参数是接口,值是给该接口提供的实现类,要写全限定的
com.xxx.xxx.service.AnimalService=com.xxx.xxx.service.DogService
#可以指定多个实现类,有多个实现类时通常会 \ 换行写,以提高可阅读性
com.xxx.xxx.service.AnimalService=\
com.xxx.xxx.service.DogService,\
com.xxx.xxx.service.CatService
#IDEA中 \ 换行时会自动缩进,行首缩进也行
com.xxx.xxx.service.AnimalService=\
com.xxx.xxx.service.DogService,\
com.xxx.xxx.service.CatService
解析 spring.factories 时会自动剔除行首的空白字符、行尾的 \
如果 spring.factories 文件的图标不对,不是spring的绿色小叶子,多半是 META-INF、spring.factories 单词拼错了,或者连词线不是英文的。
4、使用者模块中的使用示例
//参数都是指定接口、要使用的类加载器,类加载器可以为null
//获取指定接口指定的各个实现类的类名列表
List<String> classNameList = SpringFactoriesLoader.loadFactoryNames(AnimalService.class, null);
//获取指定接口指定的各个实现类的实例列表,本质是通过反射调用实现类的无参构造器创建实例
List<AnimalService> instanceList = SpringFactoriesLoader.loadFactories(AnimalService.class, null);
如果spring提供的扩展配置,比如 ApplicationListener,已经提供了接口、解析处理,我们直接写实现类、在spring.factories中写配置即可。
总结
jdk的spi机制、spring的factories机制都是插拔式设计,常用于
接口可能要使用一个或多个实现类,但具体要使用的实现类尚不确定或者可能会切换、变更
以配置文件的方式指定要使用的具体实现类,根据配置文件动态加载要使用的实现类;面向接口编程,屏蔽了底层的具体实现,更改实现时无需更改上层代码。
接口、实现类通常是在不同模块中,也可以在同一个模块中。