java-SPI机制

424 阅读6分钟

需求

 SPI机制(Service Provider Interface)其实源自服务提供者框架(Service Provider Framework),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。
 引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插
 场景:用于一些服务提供给第三方实现或者扩展,可以增强框架的扩展或者替换一些组件

设计

特性

  • 优点:
     使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

  • 缺点:

    • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。
    • 如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
    • 多个并发多线程使用ServiceLoader类的实例是不安全的。

使用场景

 概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
 比较常见的例子:

  • 数据库驱动加载接口实现类的加载
     JDBC加载不同类型数据库的驱动

  • 日志门面接口实现类加载
     SLF4J加载不同提供商的日志实现类

  • Spring
     Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

  • Dubbo
     Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

SPI机制的约定

    1. 在Maven工程中java/main/resources路径下的META-INF/services/目录中创建以接口全限定名命名的文件,该文件内容为Api具体实现类的全限定名
    1. 使用ServiceLoader类动态加载META-INF中的实现类
    1. 如SPI的实现类为Jar则需要放在主程序classPath中
    1. Api具体实现类必须有一个不带参数的构造方法

Usage

  • 定义接口及实现类
//1 定义一个接口:IOperation

 public interface IOperation {
        public int operation(int numberA, int numberB);
    }

//2.定义加法实现
 public class PlusOperationImpl implements IOperation {
        public int operation(int numberA, int numberB) {
            return numberA + numberB;
 }
    }

//3.定义除法实现
 public class DivisionOperationImpl implements IOperation{
        public int operation(int numberA, int numberB) {
            return numberA / numberB;
 }
     }

  • 定义测试类
public class MainTest {
    public static void main(String[] args) {
        ServiceLoader<IOperation> operations = ServiceLoader.load(IOperation.class);
        Iterator<IOperation> operationIterator = operations.iterator();
        while (operationIterator.hasNext()) {
            IOperation operation = operationIterator.next();
            System.out.println(operation.operation(6, 3));
       }
    }
}
  • 定义实现类文件,目录META-INF/services下面 新建一个文件,文件名为:接口的全限定名:com.jd.spi.IOperation,文件内容:写入实现的全限定实现类(包名+类名)com.ns.spi.DivisionOperationImpl

实现

代码分析

  • ServiceLoad加载流程 ServiceLoader实现了Iterable接口,调用其静态方法load,会创建ServiceLoader对象并返回
public static <S> ServiceLoader<S> load(Class<S> var0) {//1. 调用load方法
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();//默认使用线程contextClassLoader
    return load(var0, var1);
}
public static <S> ServiceLoader<S> load(Class<S> var0, ClassLoader var1) {
    return new ServiceLoader(var0, var1);//2. 实例化ServiceLoader对象
}
private ServiceLoader(Class<S> var1, ClassLoader var2) {
    this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null");
    this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2;
    this.reload();//3. 调用reload方法
}
public void reload() {
    this.providers.clear();
    this.lookupIterator = new ServiceLoader.LazyIterator(this.service, this.loader);//4. 实例化内部类懒加载迭代器lookupIterator
}

调用serviceLoader.iterator().hasNext,执行ServiceLoader.LazyIterator的hasNext方法

public boolean hasNext() {
    if (this.nextName != null) {
        return true;
    } else {
        if (this.configs == null) {
            try {
                String var1 = "META-INF/services/" + this.service.getName();//1. 查找配置文件地址为META-INF/services/$ServiceClassName
                if (this.loader == null) {
                    this.configs = ClassLoader.getSystemResources(var1);
                } else {
                    this.configs = this.loader.getResources(var1);
                }
            } catch (IOException var2) {
                ServiceLoader.fail(this.service, "Error locating configuration files", var2);
            }
        }
        while(this.pending == null || !this.pending.hasNext()) {
            if (!this.configs.hasMoreElements()) {
                return false;
            }
            this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement());//2. 读取配置文件地址里写入的serviceImpl名称
        }
        this.nextName = (String)this.pending.next();
        return true;
    }
}

如果hasNext返回true,调用next可获取接口实现的实例化对象

public S next() {
    if (!this.hasNext()) {
        throw new NoSuchElementException();
    } else {
        String var1 = this.nextName;//1. 获取next的serviceImpl类名
        this.nextName = null;
        Class var2 = null;
        try {
            var2 = Class.forName(var1, false, this.loader);//2. 反射获取Class对象
        } catch (ClassNotFoundException var5) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not found");
        }

        if (!this.service.isAssignableFrom(var2)) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not a subtype");
        }

        try {
            Object var3 = this.service.cast(var2.newInstance());//3. newInstance获取Object对象
            ServiceLoader.this.providers.put(var1, var3);
            return var3;
        } catch (Throwable var4) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4);
            throw new Error();
        }
    }
}

原理

打破双亲委派加载机制

  1. 双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。  一般都只是重写findClass(),这样可以保持双亲委派机制。而loadClass方法加载规则由自己定义,就可以随心所欲的加载类了

  2. 双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()  双亲委派模型的这个模型存在一些缺陷,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
     这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
     为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

//传统加载方式 1
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

//传统加载方式 2
System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

//SPI加载方式
//DriverManager有一个静态代码块loadInitialDrivers(),此方法会调用ServiceLoader.load(Class<S> service, ClassLoader loader)寻找ClassPath:META-INF/services文件夹下面java.sql.Driver的实现类
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

代码示例

github.com/ns7381/java…