「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」
SPI是什么
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
JavaSPI
要使用Java SPI,需要遵循如下约定: 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名; 2、接口实现类所在的jar包放在主程序的classpath中; 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM; 4、SPI的实现类必须携带一个不带参数的构造方法;
- Java SPI代码演示
创建com.jackgreek.dubbospi.javaspi.IShout
接口
public interface IShout {
void shout();
}
创建com.jackgreek.dubbospi.javaspi.Dog
实现类
@Override
public void shout() {
System.out.println("wang wang");
}
}
创建com.jackgreek.dubbospi.javaspi.Cat
实现类
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
- 在 src/main/resources/ 下建立 /META-INF/services 目录,新增一个以接口命名的文件 (
com.jackgreek.dubbospi.javaspi.IShout
文件),
com.jackgreek.dubbospi.javaspi.Dog
com.jackgreek.dubbospi.javaspi.Cat
- 使用 ServiceLoader 来加载配置文件中指定的实现
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
out
Connected to the target VM, address: '127.0.0.1:61147', transport: 'socket'
wang wang
miao miao
- ServiceLoader类
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
参考具体ServiceLoader具体源码,代码量不多,加上注释一共587行,梳理了一下,实现的流程如下:
1 应用程序调用ServiceLoader.load方法 ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
2 应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。 如果没有缓存,执行类的装载,实现如下:
- 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:
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;
}
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;
}
- 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
- 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
DubboSPI
Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 Dubbo 中,SPI 主要有两种用法,一种是加载固定的扩展类,另一种是加载自适应扩展类。这两种方式会在下面详细的介绍。 需要特别注意的是: 在 Dubbo 中,基于 SPI 扩展加载的类是单例的
DubboSPI简单使用
目录结构
创建一个Car
的接口并使用@SPI
标注此类
@SPI
public interface Car {
String getCarName(URL url);
}
编写两个实现类 BlackCar
public class BlackCar implements Car {
@Override
public String getCarName(URL url) {
return "black";
}
}
RedCar
public class RedCar implements Car {
@Override
public String getCarName(URL url) {
return "red";
}
}
在 src/main/resources/ 下建立 META-INF/dubbo/目录新增一个以接口命名的文件 (com.jackgreek.dubbospi.spi.impl.Car文件) com.jackgreek.dubbospi.spi.impl.Car
red=com.jackgreek.dubbospi.spi.RedCar
black=com.jackgreek.dubbospi.spi.BlackCar
编写测试类
ExtensionLoader<Car> extensionLoader1 = ExtensionLoader.getExtensionLoader(Car.class);
Car Car = extensionLoader1.getExtension("black");
System.out.println(Car);
ExtensionLoader<Car> extensionLoader2 = ExtensionLoader.getExtensionLoader(Car.class);
Car red = extensionLoader2.getExtension("red");
System.out.println(red);
out
com.jackgreek.dubbospi.spi.BlackCar@704a52ec
com.jackgreek.dubbospi.spi.RedCar@6ee52dcd
- ExtensionLoader中有三个常⽤的⽅法:
getExtension("dubbo")
:表示获取名字为dubbo的扩展点实例getAdaptiveExtension()
:表示获取⼀个⾃适应的扩展点实例getActivateExtension(URL url, String[] values, String group)
:表示⼀个可以被url激活的扩展点
ExtensionLoader<Car> extensionLoader2 = ExtensionLoader.getExtensionLoader(Car.class);
Car red = extensionLoader2.getExtension("red");
System.out.println(red.getCarName(null));
out
red
当我们调⽤上述代码,我们会将得到⼀个DubboProtocol的实例对象,但在getExtension()⽅法中, Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A 接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进 ⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。
DubboSPI⾃适应扩展点
⾃适应扩展点对象,也就是某个接⼝的代理对象是通过Dubbo内部⽣成代理类,然后⽣成代理 对象的。 额外的,在Dubbo中还设计另外⼀种机制来⽣成⾃适应扩展点,这种机制就是可以通过@Adaptive注解来 指定某个类为某个接⼝的代理类,如果指定了,Dubbo在⽣成⾃适应扩展点对象时实际上⽣成的就是 @Adaptive注解所注解的类的实例对象。 如果是由Dubbo默认实现的,那么我们就看看Dubbo是如何⽣成代理类的。
Car接口的getCarName方法上面加上 @Adaptive
@SPI
public interface Car {
@Adaptive
String getCarName(URL url);
}
添加Person接口类并写一个 getCar()
方法
@SPI
public interface Person {
Car getCar();
}
BlackPerson 实现了Person 接口并添加Car
属性
public class BlackPerson implements Person {
private Car car;
public void setCar(Car car) {
this.car = car;
}
@Override
public Car getCar() {
return car;
}
}
在 src/main/resources/ 下建立 META-INF/dubbo/目录新增一个以接口命名的文件 (com.jackgreek.dubbospi.spi.impl.Person文件) com.jackgreek.dubbospi.spi.impl.Person
black=com.jackgreek.dubbospi.spi.BlackPerson
代码中black.getCar().getCarName(url)
通过URL对象的方式完成 BlackPerson 实现类中Car的代理对象生成。通过Card代理对象调用Car.getCarName的扩展点
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person black = extensionLoader.getExtension("black");
URL url = new URL("x", "localhost", 8080);
url = url.addParameter("car", "black");
System.out.println(black.getCar().getCarName(url));