Java SPI是Java标准库提供的一种服务发现机制,它通过在classpath下约定的META-INF/services目录中,定义接口和其实现类之间的对应关系,从而动态加载目标接口的实现类。
通过一个实际例子来具体看一下
1、定义接口
public interface Animal {void sayHello();
}
2、实现类
这里搞两个实现类
public class Cat implements Animal {@Overridepublic void sayHello() {System.out.println("喵喵");}
}
public class Dog implements Animal{@Overridepublic 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 {//加载classc = 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也有一定的缺陷,不能按需加载只能全量扫描加载。例如上面我们配置了两个实现类,一次都会扫描加载。也不够灵活,不能通过一些条件去更精确的加载自己想要的服务实现类。