1.JAVA SPI
java spi的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类java.util.ServiceLoader
1.1接口
public interface UserService {
public String getName(int id);
}
1.2具体实现
public class MyUserServiceImpl implements UserService{
@Override
public String getName(int id) {
return "get -> name";
}
}
1.3SPI目录文件
位置
文件内容
com.example.spi.service.MyUserServiceImpl
1.4演示spi实现
public static void main(String[] args) {
Iterator<UserService> iterator = ServiceLoader.load(UserService.class).iterator();
while (iterator.hasNext()){
UserService service = iterator.next();
System.out.println(service.getName(11));
}
}
1.5 SPI原理
通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:
public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
2.Dubbo SPI
dubbo作为一个高度可扩展的rpc框架,也依赖于java的spi,并且dubbo对java原生的spi机制作出了一定的扩展,使得其功能更加强大。
首先,从上面的java spi的原理中可以了解到,java的spi机制有着如下的弊端:
-
只能遍历所有的实现,并全部实例化。
-
配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
-
扩展如果依赖其他的扩展,做不到自动注入和装配。
-
扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。
dubbo的spi有如下几个概念:
(1)
扩展点
:一个接口。
(2)
扩展
:扩展(接口)的实现。
(3)
扩展自适应实例:
其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo会根据接口中的参数,自动地决定选择哪个实现。
(4)
@SPI
:该注解作用于扩展点的接口上,表明该接口是一个扩展点。
(5)
@Adaptive:
@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。
dubbo的spi也会从某些固定的路径下去加载配置文件,并且配置的格式与java原生的不一样,类似于property文件的格式:
firstFilter=com.example.dubbo.ProviderHelloFilter
下面将基于dubbo去实现一个简单的扩展实现。首先,要实现
org.apache.dubbo.rpc.Filter
这个接口,当然这个接口是被注解@SPI标注的可以扩展的:
@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class ProviderHelloFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
System.out.println("hello ok====================================>>>>>");
return invoker.invoke(invocation);
}
}
然后,需要在duboo SPI的扫描目录下,添加配置文件,注意配置文件的名称要和扩展点的接口名称对应起来:
文件内容
firstFilter=com.example.dubbo.ProviderHelloFilter
还需要在dubbo的spring配置中显式的声明,使用上面自己实现的filter:
<dubbo:provider owner="xxx" timeout="6000" version="1.0.0" retries="0" executes="1000" filter="firstFilter"/>
最后,启动dubbo,调用service验证。
至此,dubbo的spi的demo也完成了。
dubbo spi的原理和jdk的实现稍有不同,具体实现类是
com.alibaba.dubbo.common.extension.ExtensionLoader
1.先调用loadExtensionClasses方法从下面三个目录来加载接口的实现类名
META-INF/dubbo/internal/
META-INF/dubbo/
META-INF/services/
2.调用createExtension方法反射实例化实现类对象并缓存起来
3.最后在调用filter的时候使用name找到对应的实现调用
3.总结
关于spi的详解到此就结束了,总结下spi能带来的好处:
-
不需要改动源码就可以实现扩展,解耦。
-
实现扩展对原来的代码几乎没有侵入性。
-
只需要添加配置就可以实现扩展,符合开闭原则。