Java SPI是Java标准库提供的一种服务发现机制,它通过在classpath下约定的META-INF/services目录中,定义接口和其实现类之间的对应关系,从而动态加载目标接口的实现类。
通过一个实际例子来具体看一下
1、定义接口
public interface Animal {
void sayHello();
}
2、实现类
这里搞两个实现类
public class Cat implements Animal {
@Override
public void sayHello() {
System.out.println("喵喵");
}
}
public class Dog implements Animal{
@Override
public void sayHello() {
System.out.println("汪汪");
}
}
3、添加配置文件
在resources目录下创建文件夹META-INF/services。
然后在该目录下创建一个文件,以接口的全包名为文件名
如我们这里的Animal接口就要创建
com.test.Animal文件,然后文件内容为实现类全路径名:
这里是上面两个Dog和Cat实现类
com.test.Cat
com.test.Dog
一般情况下是要将当前工程打包成jar包方式供其它三方进行引用,这里为了简单就只在同一个工程下演示了。
4、接口调用
调用通过JDK提供的工具类ServiceLoader来完成
ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
serviceLoader.forEach(Animal::sayHello);
这里最后Cat和Dog两个实现类的sayHello方法都会被调到。
5、原理分析
读取就不用说了,就是根据接口全名读取其对应的配置文件。然后加载类是通过Class.forName方式
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {//加载class
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {//实例化
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
}
这里看到是调用无参构造函数进行实例化。
使用Java SPI 可以在不引入任何三方框架前提下实现解耦,接口的定义与具体业务实现分离开来。并且在不改变原逻辑情况下,通过修改配置实现类来修改实现。
但是SPI也有一定的缺陷,不能按需加载只能全量扫描加载。例如上面我们配置了两个实现类,一次都会扫描加载。也不够灵活,不能通过一些条件去更精确的加载自己想要的服务实现类。