蹲厕所的熊

benjaminwhx

谈谈Java的SPI机制

2018-06-20 作者: 吴海旭


  1. 前言
  2. SPI机制
    1. 使用
    2. 一探ServiceLoader源码
    3. JDBC中的SPI机制

前言

SPI(Service Provider Interface)是JDK内置的一种提供服务发现的机制。如果你读过dubbo的源码,你就一定对SPI机制不陌生,Dubbo基于SPI机制提供了很多扩展功能,实现了微内核+插件的体系。如果你没有用过dubbo,没关系,JDBC你总该用过吧,还记得创建连接的写法吗?

Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

只需要一行代码,(都不需要指定Class.forName(“com.mysql.jdbc.Driver”);) 再提供商不同厂商的jar包,就可以轻松创建连接了,这其中的奥秘就要归功于SPI机制。

SPI机制

在java中根据一个子类获取其父类或接口信息非常方便,但是根据一个接口获取该接口的所有实现类却没那么容易。有一种比较笨的办法就是扫描classpath下所有的class与jar包中的class,接着用ClassLoader加载进来,再判断是否是给定接口的子类。但是这种方法的代价太大,一般不会使用。

根据这个问题,java推出了ServiceLoader类来提供服务发现机制,动态的为某个接口寻找服务实现,这种机制有点类似IOC思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

当服务的提供者提供了服务接口的一种实现之后,必须根据SPI约定在 META-INF/services/ 目录里创建一个以服务接口命名的文件,该文件里写的就是实现该服务接口的具体实现类。当程序调用ServiceLoader的load方法的时候,ServiceLoader能够通过约定的目录找到指定的文件,并装载实例化,完成服务的发现。

我们通过一个例子来加深对SPI机制的理解:

使用

首先我们提供一个接口类 Animal 以及它的两个实现类 Dog和Pig (都在包com.github.spi下):

public interface Animal {
    void eat();
}

public class Dog implements Animal {

    @Override
    public void eat() {
        System.out.println("Dog eating...");
    }
}

public class Pig implements Animal {

    @Override
    public void eat() {
        System.out.println("Pig eating...");
    }
}

接着在classpath下创建文件夹 META-INF/services ,在文件夹中新建一个文件 com.github.spi.Animal 并在文件中写入具体的实现类:

com.github.spi.Pig
com.github.spi.Dog

接着,我们就可以利用ServiceLoader进行服务发现了:

public class SPITest {

    public static void main(String[] args) {
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        Iterator<Animal> iterator = load.iterator();
        while (iterator.hasNext()) {
            Animal animal = iterator.next();
            animal.eat();
        }
    }
}

结果验证了我们的猜想,dog和pig的eat方法依次被调用了。我们顺着好奇心看看load方法是如何实现的。

一探ServiceLoader源码

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前调用线程的类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

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

load方法仅仅获取了当前调用线程的类加载器实例化之后就返回了。我们接着看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();
        }

    };
}

hasNext和next方法都调用了lookupIterator,这个类是在load的时候调用构造函数实例化的时候初始化的。看类名就能知道它是懒加载的意思(LazyIterator):

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();
    // 初始化LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}

我们直接看LazyIterator里对应hasNext和next的两个方法:

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 {
             // 通过类加载器加载classpath:META-INF/services/serviceName
            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;
        }
         // 解析文件里的值,多行为多个值,返回一个Iterator
        pending = parse(service, configs.nextElement());
    }
     // 下一个要获取的实现类类名全称
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
     // 拿到hasNext赋值的nextName
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
         // 加载类
        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里面,下次遍历的时候可以直接用
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x);
    }
    throw new Error();          // This cannot happen
}

系统的ServiceLoader通过返回一个Iterator对象能够做到对服务实例的懒加载,只有当调用iterator.next()方法时才会实例化下一个服务实例,只有需要使用的时候才进行实例化。

看到这里,应该明白了ServiceLoader所干的事了。首先根据约定的包获取到对应的接口文件,接着解析出文件中的所有服务实现类并加载实例化。

JDBC中的SPI机制

回到之前的一个问题,为什么只需要下面的一行代码,再提供商不同厂商的jar包,就可以轻松创建连接了呢?

Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

DriverManager中有一个静态代码块,在调用getConnection之前就会被调用。

static {
    loadInitialDrivers();
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                 // 1、处理系统属性jdbc.drivers配置的值
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            // 2、处理通过ServiceLoader加载的Driver类
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            // 加载配置在META-INF/services/java.sql.Driver文件里的Driver实现类
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // 忽略异常
            }
            return null;
        }
    });

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    for (String aDriver : driversList) {
        try {
             // 3、加载driver类
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
        }
    }
}

这个代码主要分三块来看:

第一部分:当用户配置了 jdbc.drivers 时,获取到对应的值,步骤3有用。

第二部分:通过ServiceLoader加载Driver类,得到所有不同数据库厂商的Driver类。

比如你引入了mysql的jar包 mysql-connector-java ,打开jar包后会发现它按照ServiceLoader的要求提供了 META-INF/services 包,并且包下面有一个叫 java.sql.Driver 的文件,文件的内容为:com.mysql.jdbc.Driver ,当然如果你还引入了oracle的jar包,你会发现它也有一个一样的文件,不过文件的内容为:oracle.jdbc.OracleDriver 。也就是load这一步会把所有配置的Driver类都获取到。

接着拿到所有的驱动类后进行迭代并调用next加载驱动类,所以触发了类加载,我们以mysql的Driver类来看:

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

它反过来调用了DriverManager的registerDriver方法来注册了自己的Driver类。

// 缓存已注册的Driver类
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
    registerDriver(driver, null);
}

public static synchronized void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        throw new NullPointerException();
    }
}

因为注册Driver类是一次性操作,后面都是读操作,所以这里用了CopyOnWriteArrayList这样一个应用在读多写少场景的并发List。

第三部分:如果拿到了第一部分的值,根据 : 拆分多个驱动实现类,并手动调用Class.forName进行类的加载,从而让不同的驱动类调用刚才说过的registerDriver方法。

因为第二部分已经做过同样的事,所以用户没有必要配置 jdbc.drivers

回到最开始的调用,我们来看看getConnection方法,因为所有的获取连接都会调用下面这个方法,这里就只列出它来。

private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
    // 获取调用类的ClassLoader
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }

    SQLException reason = null;

    for(DriverInfo aDriver : registeredDrivers) {
        // 校验调用类是否有权限加载Driver
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                 // 调用connect建立连接
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        }
    }

    // if we got here nobody could connect.
    if (reason != null)    {
        throw reason;
    }

    throw new SQLException("No suitable driver found for "+ url, "08001");
}

首先拿到调用类的classLoader和缓存的驱动类的classLoader进行比较,是同一个classLoader才放行,继续调用connect建立连接,下面是isDriverAllowed的源码。

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
             // 使用传入的类加载器进行driver类的加载,后面会把加载完的类和driver的类进行比较
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }

         // 比较是否是同一个类加载器加载的class,如果是同一个返回true允许加载driver,否则不允许
        result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

最后我们回过头来看看这发生的一切,是不是豁然开朗了许多。JDBC使用了SPI机制,让所有的任务都交给不同的数据库厂商各自去完成,无论是实现Driver接口,还是SPI要求的接口文件,都做到了让用户不需要关心一点细节,一行代码建立连接,So Cool~



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论