java 的SPI机制

1、什么是SPI机制

SPI是Service Provider Interface的简写,是JDK内置的一种服务提供发现机制,是一种扩展机制。常见的框架使用SPI机制方式:

  • JDBC驱动加载:可以根据不同的数据库厂商引入不同的JDBC驱动实现
  • springboot的SPI机制:可以在spring.factories中加入自定义的配置类、事件监听和初始化器等
  • Dubbo的SPI机制:提供了如集群扩展,路由扩展和负载扩展等扩展点,可以自有替换原有的功能配置。

2、如何使用SPI

  • 首先需要定义一个接口
  • 针对该接口,实现出类
  • 在jar包的resources目录下,新建META-INF/services目录,并创建“接口全限定名”为命名的文件,内容为实现类的全限定名
  • 主程序通过java.util.ServiceLoder动态装载实现类,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
  • SPI的实现类必须携带一个不带参数的构造方法;

3、Java SPI的源码分析

首先看ServiceLoader类源码

public final class ServiceLoader implements Iteriable{
    private static final String PREFIX = "META-INF/services/";
    private final class service;
    private final ClassLoader loader;
    private final AccessControlContext acc;
    private LinkedHashMap providers = new LinkedHashMap<>();
    private LazyIterator lookupIterator;

    private ServiceLoader(Class svc, ClassLoader cl){
        service = Objects.requireNonNull(svc, "");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public static  ServiceLoader load(Class svc){
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(srv, cl);
    }

    public static  ServiceLoader load(Class svc, ClassLoader cl){
        return new ServiceLoader(svc, cl);
    }
    
    public void reload(){
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

    private static class LazyIterator implements Iterator{
        Class service;
        ClassLoader loader;
        Enumeration configs = null;
        Iterator pending = null;
        String nextName = null;

        private LazyIterator(Class services, ClassLoader loader){
            this.service = service;
            this.loader = loader;
        }
        
        public boolean hasNext(){....}

        public S next(){...}
        
        public void remove(){...}
    }

}

接看reload 方法,其中使用到了LazyIterator对象,该对象的作用是在使用到定义的SPI类时才会去加载接口实现。

public boolean hasNext(){
    if(acc == null){
        return hasNextService();
    }else{
        PrivilegedAction action = new PrivilegedAction<>(){
            public Boolean run(){return hasNextService();}
        }
        return AccessController.doPrivileged(action, acc);
    }
}

public boolea 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);
            }
        }...
    }
    while((pending == null)|| !pending.hasNext()){
        if(!configs.hasMoreElements()){
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

从hasNext()方法可以看到,只会去META-INF/services/目录下取出一个服务提供者实现类的全限定名赋值给nextName;这里使用的懒加载,是在用到真正的服务的时候才会去加载服务提供者。

再往下看LazyIterator的next()方法,

public S next(){
    if(acc == null){
        return nextService();
    }else{
        ...
    }
}

public S nextService(){
    if(!hasNextService()){
        throw new NoSuchElementException();
    }
    String cn = nextName;
    nextName = null;
    Class c = null;
    try{
        c= Class.forName(c, flase, loader);
    }...
    
    try{
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    }...
}

SPI的接口是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的,SPI的实现类是由系统类加载器(System ClassLoader)来加载的。具体的Demon有:

public interface IHello{
    String getHello(String name);
}

创建目录:

- src
    -main
        -resources
            - META-INF
                - services
                    - com.my.test.IHello

目录内容:com.my.test.IHelloService

使用 ServiceLoader 来加载配置文件中指定的实现。

public class SPIMain {
    public static void main(String[] args) {
        ServiceLoader ih = ServiceLoader.load(IHello.class);
        for (IHello s : ih) {
            s.hello();
        }
    }
}

3、Dubbo的SPI机制

dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)扩展点发现机制加强而来,主要实现是在ExtensionLoader类中。当接口上打了@SPI注解时,dubbo会在META-INF/dubbo/internal/接口全名文件下读取扩展点.配置文件内容为name->implementation class,可以声明多个提供者。

Java SPI的缺点

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,即接口的实现类全部加载并实例化一遍
  • 获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用ServiceLoader类的实例是不安全的。
  • 如果扩展点加载失败,出现的错误信息不明确,连扩展点的名称都拿不到。

dubbo SPI 的区别:

  • dubbo spi增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
  • JDK的spi是通过System ClassLoader,Dubbo spi是通过Extend ClassLoader

你可能感兴趣的:(java,框架技术学习)