Dubbo-Spi机制

422 阅读6分钟

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战

SPI是什么

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。 image 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 扩展加载的类是单例的 image

DubboSPI简单使用

目录结构 image

创建一个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接⼝的⼀个代理对象。 image

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));

Dubbo-Spig官方文档