SPI在java中是Service Provider Interface的缩写,服务提供者接口的意思,最早,这个方式是用来针对第三方提供的服务或者扩展的,也就是各个厂商或者插件。系统只提供抽象接口,并不实现,而由厂商提供实现,每一个厂商都有自己的实现,当我们需要调用这个接口服务,我们只需要指定接口,系统会动态加载各种实现并初始化。
SPI机制也需要遵循一个规范:厂商提供接口实现,最终打包的时候,需要指定这个实现所在的位置,以及实现类的全限定名,需要在类路径下的META-INF/services/目录下创建一个以接口名命名的文件,文件中指定实现类的全限定名即可。当外部程序需要装配和这个模块的时候,就能通过该jar包/META-INF/services/里的配置文件找到具体的实现类,并装载实例化,完成模块的注入。
寻找接口的实现类,即服务发现的过程,有点类似IOC,将装配的控制权移到程序之外,这也是模块化的设计思想。jdk提供了服务发现的工具类:java.util.ServiceLoader。通过该类,我们可以查找所有的实现类。
SPI实现机制如下图所示:
若要满足SPI的实现,需要构造这样的项目,不仅要实现接口,还需要在MATA-INF/services/目录下构建一个接口命名的文件:
SPI的机制有点类似策略模式,接口类提供抽象接口,不提供具体实现,实现类实现具体的接口,一个实现类就是一种方案。SPI实际上就是基于“接口的编程+策略模式+配置文件”实现的服务动态加载机制。
下面通过具体的实例来演示:
创建两个项目,一个是提供服务(即接口实现)的项目,一个是服务调用方项目,我们在调用方会引入接口实现项目依赖。
接口实现项目结构如下:
工程内容很简单,就是定义一个接口,两个实现类,然后配置实现类的全限定名。
接口类:GreetService.java
package com.xxx.service;
public interface GreetService {
void say(String name);
}
实现类:HelloService.java
package com.xxx.service;
public class HelloService implements GreetService {
public void say(String name) {
System.out.println("hello2,"+name);
}
}
实现类:GoodbyeService.java
package com.xxx.service;
public class GoodbyeService implements GreetService{
public void say(String name) {
System.out.println("goodbye,"+name);
}
}
META-INF/services/com.xxx.service.GreetService
com.xxx.service.HelloService
com.xxx.service.GoodbyeService
项目打包,然后在服务调用项目中引入这个依赖,建立一个测试类,我们简单测试一下:
package com.xxx.spi;
import java.util.ServiceLoader;
import com.xxx.service.GreetService;
public class ServiceProviderInterfaceTest {
public static void main(String[] args) {
ServiceLoader<GreetService> services = ServiceLoader.load(GreetService.class);
for(GreetService service:services){
service.say("xxx");
}
}
}
运行程序,打印结果如下:
在实际中使用SPI机制的的有很多,比如mysql驱动,我们看看mysql-connector-java驱动的包结构:
因为它实现了java.sql.Driver驱动接口,在配置文件中配置了实现该接口的类有两个,其中一个就是我们常用的com.mysql.jdbc.Driver类。
我们的刚才调用方测试接口中使用了jdk提供的ServiceLoader工具类,我们根据这个思想,自己实现一个服务查找的类:
1、通过文件查找,找到实现类的全限定名。
2、通过全限定名来反射找到实体类。
3、实体类通过newInstance()实例化,然后加入一个集合。
一个简单的服务加载类如下:
package com.xxx.spi;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import com.xxx.service.GreetService;
public class SimpleServiceLoader {
private static final String PREFIX="/META-INF/services/";
@SuppressWarnings("unchecked")
public static <T> List<T> load(Class<T> cls){
List<T> list = new ArrayList<T>();
List<String> classList = read(cls);
for(String className:classList){
Class<T> c = null;
try {
c= (Class<T>)Class.forName(className);
list.add(c.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
return list;
}
public static List<String> read(Class<?> cls){
String file = cls.getCanonicalName();
String name = cls.getResource(PREFIX+file).getPath();
List<String> list = new ArrayList<String>();
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(new File(name)));
String line = "";
while((line = reader.readLine())!=null){
list.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}finally{
if(reader!=null){
try {
reader.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
return list;
}
public static void main(String[] args) {
List<GreetService> services = load(GreetService.class);
for(GreetService service:services){
service.say("xxx");
}
}
}
运行该程序,打印信息如下:
hello2,xxx
goodbye,xxx
结果和使用ServiceLoader工具类加载是一样的。