目录
什么是SPI
SPI(Service Provider Interface)
是 JDK 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
SPI和API的使用场景
API (Application Programming Interface)在大多数情况下,都是实现方
设计接口并完成对接口的实现,调用方
仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)
则是调用方
来制定接口规范,提供给外部来实现,调用方在调用时则
选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
SPI的简单实现
下面简单实现一个 JDK 的 SPI 的简单demo
1、工程结构如下
2、代码
【UserDao】
package org.ailun;
/**
* 接口
*/
public interface UserDao {
String getUser();
}
【UserDaoImplA】
package org.ailun;
/**
* 实现A
*/
public class UserDaoImplA implements UserDao {
@Override
public String getUser() {
return "UserDaoImplA";
}
}
【UserDaoImplB】
package org.ailun;
/**
* 实现B
*/
public class UserDaoImplB implements UserDao {
@Override
public String getUser() {
return "UserDaoImplB";
}
}
【SpiTest】
package org.ailun;
import java.util.ServiceLoader;
/**
* 测试入口
*/
public class SpiTest {
public static void main(String[] args) {
System.out.println("Hello world!");
ServiceLoader<UserDao> userDao = ServiceLoader.load(UserDao.class);
for (UserDao dao : userDao) {
System.out.println("######################### \t[ " + dao.getUser() + " ]\t #########################");
}
}
}
3、资源目录
然后需要在resources
目录下新建META-INF/services
目录,并且在这个目录下新建一个与上述接口类全限定名一致 文件名 的 文件,在这个文件中写入接口的实现类的全限定名
【org.ailun.UserDao】
org.ailun.UserDaoImplA
org.ailun.UserDaoImplB
4、启动测试类输出如下内容
Hello world!
######################### [ UserDaoImplA ] #########################
######################### [ UserDaoImplB ] #########################
这样一个简单的SPI的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。
SPI原理解析
通过上面简单的demo,可以看到最关键的实现就是ServiceLoader类中LazyIterator
的这个内部类,可以看下这个类的加载相关的核心源码,如下:
// Private inner class implementing fully-lazy provider lookup
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// 加载定义在 resources 目录下新建 META-INF/services 目录下的SPI配置文件内容
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
// 根据 hasNextService 方法加载的SPI配置的接口实现类的全类名 的内容 按行加载接口的实现类并实例化
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
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;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
// 提供迭代的入口,从这里看
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() {
return hasNextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}
// 最终返回接口实现类的实例
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() {
return nextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
上面的代码只贴出了部分关键的实现,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:
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文件的格式:
下面将基于Dubbo去实现一个简单的扩展实现。首先,要实现LoadBalance
这个接口,当然这个接口是被注解标注的可以扩展的:
@SPI("random")
public interface LoadBalance {
@Adaptive({"loadbalance"})
<T> Invoker<T> select(List<Invoker<T>> a, URL b, Invocation c) throws RpcException;
}
【实现LoadBalance】
public class myLoadBalance implements LoadBalance {
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
System.out.println("my demo loadBalance is used, hahahahh");
// 选择第一个
return invokers.get(0);
}
}
然后,需要在Dubbo SPI的扫描目录下,添加配置文件,注意配置文件的名称要和扩展点的接口名称对应起来:例如META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance
还需要在dubbo的spring配置中显式的声明,使用上面自己实现的负载均衡策略:
<dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" />
至此,Dubbo 的SPI的demo完成。
Dubbo SPI的原理和 JDK 的实现稍有不同,大概流程如下图,具体的实现读者可以自己了解下源码。
主要看
类:ExtensionLoader
方法:public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)
、public T getExtension(String name)
总结
关于SPI的详解到此就结束了,总结下SPI能带来的好处:
- 不需要改动源码就可以实现扩展,解耦。
- 实现扩展对原来的代码几乎没有侵入性。
- 只需要添加配置就可以实现扩展,符合开闭原则。
参考:https://blog.csdn.net/cocoa_geforce/article/details/117369236
标签:Java,实现,扩展,接口,SPI,META,return,public From: https://www.cnblogs.com/hhddd-1024/p/17464806.html