ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

SPI——Service Provider Interface

2022-07-11 17:00:17  阅读:153  来源: 互联网

标签:Service Driver ServiceLoader SPI API 线程 Interface public 加载


从一个示例开始

下面是一个用于打印的接口,它会将message打印到控制台,但以什么格式打印是实现类规定的。

package top.yudoge.springserials.basic.spi;

public interface Printer {
    void print(String message);
}

LinePrinter简单的调用System.out.println,让每次打印在一个新的行中:

package top.yudoge.springserials.basic.spi.impl;

import top.yudoge.springserials.basic.spi.Printer;

public class LinePrinter implements Printer {
    @Override
    public void print(String message) {
        System.out.println(message);
    }
}

SquarePrinter将要打印的message用方块包裹起来:

package top.yudoge.springserials.basic.spi.impl;

import top.yudoge.springserials.basic.spi.Printer;

public class SquarePrinter implements Printer {

    @Override
    public void print(String message) {
        // print top edge
        printNTimes("#", message.length() + 2);
        // print side and message
        System.out.println("|" + message + "|");
        // print bottom edge
        printNTimes("#", message.length() + 2);
    }

    public void printNTimes(String string, int times) {
        for (int i=0; i<times; i++)
            System.out.print(string);
        System.out.println();
    }

}

Java提供的ServiceLoader类会扫描类路径下的META-INF/services/中的文件,文件名是接口名,文件中的每一行是一个该接口的实现类:

下面是resources/META-INF/services/top.yudoge.springserials.basic.spi.Printer文件中的内容:

top.yudoge.springserials.basic.spi.impl.SquarePrinter

这里我们只写了SquarePrinter,下面看看如何使用ServiceLoader构建这个实现类:

public class Main {
    public static void main(String[] args) {
        ServiceLoader<Printer> printers = ServiceLoader.load(Printer.class);
        for (Printer printer : printers) {
            printer.print("hello^_^");
        }
    }
}

结果:

img

修改META-INF/service下的文件:

top.yudoge.springserials.basic.spi.impl.LinePrinter
top.yudoge.springserials.basic.spi.impl.SquarePrinter

重新运行,ServiceLoader加载了两个实现类:

img

有什么用?

这就是SPI?这破玩意儿的实际用处在哪?

在这个例子中,我们确实看不到实际用处,但是,让我们来通过分析JDBC程序来看看它的作用。

通过JDBC来访问数据库通常需要这么两步:

//1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

//2、通过DriverManager获取数据库连接对象
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/sys", "root", "密码");

先解释一下和SPI无关的,为什么Class.forName能注册驱动

进入到MySQL的Driver实现中,可以看到这样的代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        DriverManager.registerDriver(new Driver());
    }
}

所以,是该类被加载之后触发了里面的静态代码块,静态代码块调用JDK中的DriverManager完成了驱动的注册

不手动注册驱动行吗?

下面我们写这样的代码,我们没在任何位置手动注册了MySQL驱动:

public class Main {
    public static void main(String[] args) throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbconcept", "root", "root");
        System.out.println(connection);
    }
}

但是结果表明,连接还是建立成功了:

img

是ServiceLoader在工作

我们查看DriverManager的代码,发现如下静态代码块:

/**
* 通过检查系统属性——`jdbc.properties`和使用ServiceLoader机制
* 加载默认JDBC驱动
*/
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

然后loadInitialDrivers方法里面有这样一段:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}

这不就是对每一个classpath下的META-INF/services下的文件名为java.sql.Driver的文件中的所有Driver实现类的全限定名进行类加载吗?

而MySQL的类路径下确实有这个文件:

img

里面的内容就是这个驱动

img

所以,流程是:

  1. DriverManager类被你使用,所以它的类初始化代码被执行
  2. 它的类初始化代码中使用ServiceLoader查找类路径下META-INF/server/java.sql.Driver中描述的Driver实现类
  3. 对使用迭代器迭代每一个Driver实现类,这样,实现类就会被加载
  4. MySQL实现类的类初始化代码中调用了DriverManager.registerDriver

可是!到底有什么用?

考虑上面的数据库驱动的场景,这个场景中有三个角色:

  1. API定义者:Java定义了Driver的API
  2. API实现者:数据库厂商实现Driver,如MySQL
  3. API使用者:我们

在这里,API定义者对API使用者使用何种实现一无所知,它可能有无数种实现,并且该数目可能会随时间不断扩张。

在以往的开发中,API定义者和API实现者往往是一起的,比如最初的Printer例子:

img

在这种场景下,API定义者对系统中可能存在的实现很清楚,API调用者通常会直接使用定义者提供的实现类,而不是自己指定一个新的实现类。

而对于JDBC的例子,API调用者必须提供一个API实现者提供的实现类,因为在这种场景下,API定义者、API实现者是分离的。

而SPI机制规定了API实现者如何向系统中递交自己的实现类,该实现类的发现是自动的(一般是由某种框架扫描发现,比如DriverManager),由于实现类会被自动发现,所以API调用者的代码只需使用API定义者提供的接口,而无需显式的编写、创建API实现者的实现类。这让它可以极其方便的替换实现。

对于上面一段话,可以想象,使用JDBC时,如果想从MySQL切换到Oracle,是不是直接替换驱动、修改数据库地址即可,因为你的代码中从来没有显式的依赖任何MySQL驱动中的类,只是在用JDBC规范中的接口在工作。

再想想,如果你的类中依赖的不是java.sql.Driver接口,而是遍地都是com.mysql.cj.jdbc.Driver这个实现类,那么你换到Oracle的时候可能就有得忙活了。

使用SPI机制加载的类,到底何时、如何被加载?

下面我们通过分析代码来分析标题的问题。

img

上面,Java的DriverManager中调用了ServiceLoader.load(java.sql.Driver.class)后,得到了一个ServiceLoader对象,这个对象是个可迭代对象,随后DriverManager又对它进行了迭代。

img

从这个迭代过程中啥也没干,就大概可以说明ServiceLoader.load中并未实际的加载那些实现类,而那些实现类在被迭代时加载,否则为什么要进行这个迭代呢?

ServiceLoader.load干了什么?

下面是ServiceLoader.load(Class)方法的代码,它获取了调用者线程的线程上下文类加载器,并且调用了重载方法,把这个线程上下文类加载器传了进去:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

下面是重载的load方法,它只是简单的创建了ServiceLoad对象并返回:

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

目前都未发生任何对实现类的加载,关于线程上下文类加载器,我也不知道是个啥,再点到ServiceLoader的构造方法中看看:

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

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

只是做了检查,如果classLoader是null的话使用系统类加载器,也就是AppClassLoader,但注意,这里cl并不为null,是调用者线程的线程上下文类加载器,稍后我可能会查什么是线程类上下文加载器。

被构造器调用的reload方法看起来也很简单,它只是初始化了一个什么迭代器对象。

如上是通过调用ServiceLoader的静态方法load来返回一个ServiceLoader实例的整个调用链,这个调用链中没有对实现类进行加载的代码。所以,对那些Driver实现的加载,必定在稍后的遍历过程中。

我们简单看下ServiceLoader.iterator方法返回的迭代器:

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

这里引入了一个什么knownProviders,迭代器先对这个进行迭代,之后再对我们在构造方法中看到的lookupIterator进行迭代。可能有点令人迷惑,不过注意,knownProviders是基于providers这个东西创建的,这个东西已经在构造方法中被我们清空了,所以在上面DriverManager的调用链中,我们可以完全忽视迭代过程中knownProviders所起的作用,所以代码被简化成了这样:

public Iterator<S> iterator() {
    return new Iterator<S>() {

        public boolean hasNext() {
            return lookupIterator.hasNext();
        }

        public S next() {
            return lookupIterator.next();
        }

    };
}

那么,我们就该看lookupIterator了,因为这明显是对它的一次委托,它是一个LazyIterator,看起来是个什么懒加载的迭代器,我们简化了它的hasNext的代码:

public boolean hasNext() {
    return hasNextService();
}

进入hasNextService

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // === core start ===
            // PREFIX="META-INF/services/"
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
            // === core end ===
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        // === core start ===
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
        // === core end ===
    }
    nextName = pending.next();
    return true;
}

上面,只需要看我的注释中间的部分,总的来说,就是去META-INF/service下找指定文件(根据传入ServiceLoader的类名),然后用classloader去加载这个文件,这个classloader在我们的调用链中就是调用者线程上下文ClassLoader。

得到文件之后,调用parse对每一行进行一个解析,总之,LazyIterator的hasNext的作用就是判断那个文件中是否还有行,如果有,把这一行解析出来,设置给nextName,供稍后迭代器的next方法使用。而pending就是hasNextnext中交换状态的一个变量,有了它,hasNext不用在连续调用hasNext时重复解析同一行。

上面的这一段都是我猜的,我没力气分析这些代码,所以咱们看个大概,这些算法的细节在我们理解源码的过程中并没什么作用。

LazyIterator的next方法调用了nextService方法,直接看:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 核心代码,初始化类,使用了loader
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
                "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
                "Provider " + cn  + " not a subtype");
    }
    try {
        // 核心代码,创建对象
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
                "Provider " + cn + " could not be instantiated",
                x);
    }
        throw new Error();          // This cannot happen
    }

所以,实际的类加载发生在迭代过程中的next时,在这个时候,加载这个类,创建该类的一个实例,并且:

  1. 加载类时使用了外部传入的classLoader,外部没传入的时候就是那个线程上下文类加载器
  2. 想使用SPI机制的类必须有无参构造,因为这里的newInstance没传入参数

线程上下文类加载器到底是什么?

下面内容来自知乎文章java ContextClassLoader (线程上下文类加载器)

java.lang.Thread中的方法getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。

如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,Thread默认继承父线程的Context ClassLoader(注意,是父线程不是父类)。如果你整个应用中都没有对此作任何处理,那么所有的线程都会以System ClassLoader作为线程的上下文类加载器。

所以,我们可以确定,在我们没设置线程上下文类加载器的时候,使用的是AppClassLoader

下面的代码,首先SPI机制隐式的加载了MySQL Driver实现类,然后我们获得了它,并打印了它的类加载器,又打印了当前线程的类加载器:

public class Main {
    public static void main(String[] args) throws SQLException {
        Driver driver = DriverManager.getDriver("jdbc:mysql://localhost:3306/dbconcept");
        System.out.println(driver.getClass().getClassLoader());

        System.out.println(Thread.currentThread().getContextClassLoader());
    }
}

由于隐式加载Driver类的也是这个Main线程,所以它俩是一个线程,那么上下文加载器也一样,所以结果都是AppClassLoader

img

绕这么一圈干嘛?

想想被DriverManager直接加载的类会是由什么类加载器加载?

DriverManager是Java核心组件,它是由BootstrapClassLoader加载的,那么它所直接加载的类也都会由BootstrapClassLoader进行加载。但是,BootstrapClassLoader只能加载%JAVA_HOME%/lib下的类,它自然无法加载驱动实现厂商给的类,而且,它也没有任何办法拿到底层的AppClassLoader,这是双亲委派模型的单向限制。

img

所以,所以!相当于调用者线程的线程类加载器将AppClassLoader传递进来,然后再用它来加载驱动厂商的实现。

所以,线程上下文加载器就是用来解决Java中的核心库反向加载其它由Java用户提供的类时,让它把底层ClassLoader传递进来的一个不太优雅的解决办法

标签:Service,Driver,ServiceLoader,SPI,API,线程,Interface,public,加载
来源: https://www.cnblogs.com/lilpig/p/16467010.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有