您当前的位置:首页 > 计算机 > 编程开发 > Java

Java SPI 机制介绍(since jdk 1.6)

时间:12-14来源:作者:点击数:
CDSY,CDSY.XYZ

SPI 全称为 Service Provider Interface,是JDK内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现机制。例如:有个接口想在运行时才发现具体的实现类,那么你只需要在程序运行前添加一个实现即可,并把新加的实现描述给JDK即可。此外,在程序的运行过程中,也可以随时对该描述进行修改,完成具体实现的替换。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口是由 Java 核心库来提供,而SPI的实现则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)中。例如:JDBC 的实现 mysql 就是通过 maven 被依赖进来。

Java SPI 实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以 SPI 的核心思想就是解耦。

那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的。SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。

引导类加载器在加载时是无法找到 SPI 的实现类的,因为双亲委派模型中规定,引导类加载器 BootstrapClassloader 无法委派系统类加载器 AppClassLoader 来加载。这时候,该如何解决此问题?

线程上下文类加载由此诞生,它的出现也破坏了类加载器的双亲委派模型,使得程序可以进行逆向类加载。

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

比较常见的例子:

  • 数据库驱动加载接口实现类的加载
  • JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载
  • SLF4J加载不同提供商的日志实现类
  • Spring
  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
  • Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

下面,我们就用具体的代码来说明逆向类加载。不过,在距离之前,还是想对spi的使用进行一个简单的说明。

spi 使用

1.代码编写

既然是 spi,那么就必须先定义好接口。其次,就是定义好接口的实现类。

2.创建一个文件夹

在项目的 \src\main\resources\ 下创建 \META-INF\services 目录

3.文件夹下增加配置文件

在上面 META-INF\services 的目录下再增加一个配置文件,这个文件必须以接口的全限定类名保持一致,例如:com.xxx.xxx.HelloService

4.配置文件增加描述

上面介绍 spi 时说道,除了代码上的接口实现之外,你还需要把该实现的描述提供给JDK。那么,此步骤就是在配置文件中撰写接口实现描述。很简单,就是在配置文件中写入具体实现类的全限定类名,如有多个便换行写入。

5.使用 JDK 来载入

编写 main() 方法,输出测试接口。使用 JDK 提供的 ServiceLoader.load() 来加载配置文件中的描述信息,完成类加载操作。

SPI 原理解析

ServiceLoader 这个类

public final class ServiceLoader<S> implements Iterable<S> {


    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;

    // 上下文对象
    private final AccessControlContext acc;

    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

spi 加载的主要流程供参考

spi 能带来的好处

  • 不需要改动源码就可以实现扩展,解耦。
  • 实现扩展对原来的代码几乎没有侵入性。
  • 只需要添加配置就可以实现扩展,符合开闭原则。

使用 spi 需要注意

  • 虽然 ServiceLoader 也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐