跟我学SPI之SPI详解及实战
首先了解下何为SPI,这里引用Dubbo官方对SPI的解说,比较全面。SPI简介
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
Dubbo本身实现了对JDK的SPI的扩展,为了能够更好的理解Dubbo的SPI机制,我们需要理解JDK的原生SPI原理。
从概念可以看出,SPI机制是一种服务发现机制,能够在运行时动态的增加、获取服务实现。个人感觉很像设计原则中的里氏替换原则。
我将通过一个实例来讲解如何对JDK的SPI进行运用。
首先定义接口
首先定义一个名为DemoInterface的接口,定义两个抽象方法。
public interface DemoInterface<T, P> {
T execute(P p);
String className();
}
注意:className() 实现类实现该方法,需要返回子类的全限定名。
定义实现-UserServiceImpl
为了说明问题,我们需要定义两个不同的实现类,首先定义UserServiceImpl,实现接口的方法,execute方法为通过userName构造不同的User实体并返回。
public class UserServiceImpl implements DemoInterface<User, String> {
@Override
public User execute(String userName) {
return new User().setUserId("123123123").setUserName(userName);
}
@Override
public String className() {
return UserServiceImpl.class.getCanonicalName();
}
}
className()返回了UserServiceImpl的全限定名。
定义实现-JetFighterServiceImpl
不同于UserServiceImpl,JetFighterServiceImpl为战斗机(太中二了)实现,execute方法为通过战机型号返回对应的战机实例。所有的战机实例通过static代码块加载到ConcurrentHashMap中。
public class JetFighterServiceImpl implements DemoInterface<JetFighter, String> {
private static final Map<String, JetFighter> JET_FIGHTER_MAP =
new ConcurrentHashMap<>(16);
static {
JET_FIGHTER_MAP.put("f15", new JetFighter().setJetId("f15").setJetName("F15战斗机"));
JET_FIGHTER_MAP.put("f22", new JetFighter().setJetId("f22").setJetName("F22战斗机"));
JET_FIGHTER_MAP.put("f35", new JetFighter().setJetId("f35").setJetName("F35战斗机"));
JET_FIGHTER_MAP.put("j20", new JetFighter().setJetId("j20").setJetName("J15战斗机"));
JET_FIGHTER_MAP.put("j31", new JetFighter().setJetId("j31").setJetName("J31战斗机"));
}
@Override
public JetFighter execute(String s) {
return JET_FIGHTER_MAP.get(s);
}
@Override
public String className() {
return JetFighterServiceImpl.class.getCanonicalName();
}
}
className()返回了JetFighterServiceImpl的全限定名。
通过ServiceLoader加载服务
定义好不同的接口实现,我们需要通过ServiceLoader加载服务,建立一个bean工厂,将服务实例解析后设置到bean工厂中。
public class DemoServiceFactory {
private static final Map<String, DemoInterface> serviveContext =
new ConcurrentHashMap<>();
private static final DemoServiceFactory factory = null;
private DemoServiceFactory() {
ServiceLoader<DemoInterface> serviceLoaders =
ServiceLoader.load(DemoInterface.class);
for (DemoInterface demoInterface : serviceLoaders) {
serviveContext.put(demoInterface.className(), demoInterface);
}
}
public static DemoServiceFactory getInstance() {
synchronized (Object.class) {
if(factory == null) {
synchronized (Object.class) {
if (factory == null) {
factory = new DemoServiceFactory();
}
}
}
}
return factory;
}
public DemoInterface getServiceInstance(String className) {
DemoInterface demoInterface = serviveContext.get(className);
if (demoInterface == null) {
throw new IllegalArgumentException("请输入合法的className");
}
return demoInterface;
}
}
这里解释下DemoServiceFactory工厂的实现。
DemoServiceFactory是基于线程安全懒汉模式的单例实现。首先实例化了一个ConcurrentHashMap,它就是我们的Bean容器,key为DemoInterface的实例全限定名,value为DemoInterface的具体实例。
在DemoServiceFactory的私有构造方法中,我们通过 ServiceLoader.load(Class service) 方法加载了所有的DemoInterface。
然后通过foreach遍历所有的DemoInterface的实例,并依次加载到serviveContext中,key=实例的全限定类名,value为实例。
getServiceInstance(String className) 方法通过实例的全限定名,从serviveContext中获取对应的DemoInterface的实现类的实例。
编写服务定义
最后一步,也是比较关键的步骤。
我们需要在classpath下建立META-INF,并在META-INF下建立services文件夹,并建立文件名为DemoInterface的全限定的文本文件,文件名为:com.snowalker.spi.DemoInterface
我的项目基于maven构建,因此我在resources下建立了目录 META-INF/services/ ,在该路径下建立文本文件,名为 com.snowalker.spi.DemoInterface 。
打开该文本文件,并在其中添加所有DemoInterface的实现类的全限定名,一行一个,具体的内容如下:
# DemoInterface实现类
com.snowalker.impl.UserServiceImpl
com.snowalker.impl.JetFighterServiceImpl
注释不会解析。
到此,我们的SPI开发就基本结束了,应用通过DemoServiceFactory获取实例的时候,ServiceLoader会扫描META-INF/services下的接口定义文件,并加载所有的实例。
编写测试
我们写一个测试用例测试一下上面写的demo是否能满足我们的需求–通过实现类的全限定名获取到对应的实现类实例。
代码如下:
DemoServiceFactory demoServiceFactory =
DemoServiceFactory.getInstance();
String userServiceImplClassName = "com.snowalker.impl.UserServiceImpl";
String jetFighterServiceImplClassName = "com.snowalker.impl.JetFighterServiceImpl";
DemoInterface<User, String> userInstance = demoServiceFactory.getServiceInstance(userServiceImplClassName);
DemoInterface<JetFighter, String> jetFightInstance = demoServiceFactory.getServiceInstance(jetFighterServiceImplClassName);
System.out.println("---------用户服务实例调用开始-----------");
User result = userInstance.execute("snowalker");
System.out.println(" 用户服务实例调用结束:" + result.toString());
System.out.println("---------战斗机服务调用开始--------------");
JetFighter fighter = jetFightInstance.execute("j20");
System.out.println(" 战斗机服务实例调用结束:" + fighter.toString());
我们分别获取UserServiceImpl、JetFighterServiceImpl的实例,并调用各自的execute方法。
运行测试用例,打印如下:
---------用户服务实例调用开始-----------
用户服务实例调用结束:User{userId='123123123', userName='snowalker'}
---------战斗机服务调用开始--------------
战斗机服务实例调用结束:JetFighter{jetId='j20', jetName='J15战斗机'}
Process finished with exit code 0
可以看到,调用达到预期,通过SPI的ServiceLoader,我们实现了一种更加优雅的工厂模式。
之所以说优雅,就在于我们能够通过只编写接口实现类并在META-INF/services下的接口定义文件中配置实现类的全限定名,就可以按需获取不同的接口实例。这种方式在大量的接口实现类场景下优势很明显,我们不需要实现包扫描以及实例的加载,避免了重复造轮子,而且借助SPI方式,实现更加稳定美观。
总结
实际上,SPI本质上是面向接口编程的一种体现,中间件厂商通过SPI能够实现插件的可插拔。它的优势在于在模块装配的时候不需要显式的指定实现而能够动态的寻找到服务实现,有点像IOC的机制。业务不需要关系服务装配,将服务装配的控制权反转给了框架本身。
最后再总结一下SPI的开发流程:
- 服务提供者需要提供服务接口的声明
- 服务提供者在jar包META-INF/services/目录里创建一个以服务接口命名的文件
- 服务实现方实现服务提供者发布的接口,并在META-INF/services/目录里以服务接口命名的文件中添加该实现的全限定名。
- 外部程序装配该模块时,通过jar包META-INF/services/里的配置文件就可以找到具体的实现类名,java.util.ServiceLoader将接口实现装载并实例化从而完成模块的注入。且该实例过程对于调用方是透明的,