Spi,微内核与插件化

Spi,微内核与插件化

1. Spi概述

2. 谈谈Sql驱动

3. 写一个Sql驱动

4. 通过Spi机制再写一个Sql驱动

5. 总结

  1. Spi概述
    Spi全称Service Provider Interface,是一种服务发现机制。通过在指定路径下(通常为:META-INF/services文件夹下)读取。读取的内容是接口实现类,将该实现类应用起来,具体使用和操作目的在本文后续会讲解。
    在Spi机制中,可以看做有两方角色:微内核+插件。对此我们可以联想一下我们常用的开发工具Eclipse、IDEA、VScode、Atom、Sublime Text等,无一不是拥有着较大的插件生态,这些插件时如何作用起来的?
    将IDE视为微内核,以IDE运行程序为例,运行时IDE可以去回调一个接口,作为运行程序动作触发后需要进行的操作。它可以是显示控制台、统计覆盖率、打开浏览器等。这个可以提供给插件自定义的方式,就是微内核+插件微内核拥有着一个严谨的操作,负责回调各种插件,可替换的插件有效降低了系统的耦合。

  2. 谈谈Sql驱动
    这会谈Sql驱动,是因为Sql驱动也是一个插件。我们有各种Sql驱动JDBC、ODBC...。使用它们只需要把它们引入进来,这就已经呈现出插件可替换的特点了。
    为了后面能比较顺畅的进行Sql驱动的编写,在这先提一提DriverManager如何加载插件。
    先展示下加载Sql驱动的模板语句:

public static void main(String[] args) {
    
    String classPath = "org.jdbc.driver.MyDriver";
    
    try {
        Class.forName(classPath);
        DriverManager.getConnection("url", "user", "pass");
        // ...
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

我们看看DriverManager.getConnection()的源码,究竟做了什么?

public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
        info.put("user", user);
    }
    if (password != null) {
        info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));
}

经过参数检验后,核心是调用了getConnection方法,继续看看这个方法做了什么?
代码偏多,删减一些注释和语句

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    //...删
    //这里可以看到:循环了成员变量registeredDrivers
    for(DriverInfo aDriver : registeredDrivers) {
        // ...
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    // Success!
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }

        } else {
            println("    skipping: " + aDriver.getClass().getName());
        }

    }

    // ...删
}

让我们看看这个registeredDrivers的定义

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

显然是个数组,那它是什么时候拥有Driver的信息的呢?

在获取Sql连接前,有一个需要写并且好像并不起眼的代码:

Class.forName(classPath);

将Driver加载了一次。而这操作就是为了加载驱动。为什么写了这行代码就能加载驱动?

  1. 写一个Sql驱动
    DriverManager中,有一个DriverManager.registerDriver方法,用于将指定驱动加载入DriverManager.registeredDrivers驱动列表内。Class.forName(classPath)代码会促使驱动类内静态代码块的执行,所以只要在驱动类的静态代码块中向DriverManager注册我们自己编写的Driver,就可以成功引入我们的驱动。
public class MyDriver implements Driver {
    
    private static Driver driver;
    // 加载驱动
    static {
        try {
            driver = new MyDriver();
            // 向DriverManager注册驱动
            DriverManager.registerDriver(driver);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    // ... 删
}

驱动需要实现java.sql.Driver接口,在静态代码块中注册即可实现引入驱动。至于获取连接,只需要在MyDriver.connect中返回一个Connection,就可以在DriverManager.getConnection中获取到该Connection

然而,这样的操作虽然实现了插件化(驱动就是插件),但我们每次在获取Connection前,都需要写一次Class.forName(classPath)的代码来加载驱动。那么有没有办法能够不用写这行代码呢?

  1. 通过Spi机制再写一个Sql驱动
    答案当然是有的。上述方式实现了插件化,但严格意义上不算Spi,Spi中还有一个文件夹尚未使用,这个文件夹到底有什么用处?
    从上面的驱动加载可以知道,我们需要规避的是加载驱动的这个动作,那为了在用户使用时规避这个动作,就需要一个管理类来做这个操作,并且该管理类需要获取到classPath类路径。
    所以,这个文件夹META-INF/services的意义,就是告诉读取插件的类,需要读取的插件的类路径在XXX地方(充当配置文件的意义)。
    为了充分了解整个过程,我们谈谈DriverManger中使用到的插件读取类ServiceLoader

DriverManager中,有这么一段静态代码块:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

loadInitialDrivers内部,有这么一段代码:

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

这个ServiceLoader充当的就是插件读取类的角色。
返回的loadedDrivers是一个迭代器,所以我们需要查看迭代器的方法:

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // PREFIX定义: private static final String PREFIX = "META-INF/services/";
            // 此处的service是ServiceLoader<Driver>中的Driver
            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;
        }
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    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.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

有上述代码可知,ServiceLoader在文件夹META-INF/services/+接口全类名的文件中读取插件类路径。所以利用这个Spi机制,ServiceLoader就能在迭代器获取到类信息,并且在nextService方法中帮助我们实现驱动类的加载。

所以对于使用Spi机制来加载驱动的插件提供者来说,只需要在META-INF/services/java.sql.Driver文件下,写一行插件名。

org.jdbc.driver.MyDriver

这样DriverManager就可以通过ServiceLoader实现的Spi机制,帮助我们加载驱动。

对于Spi机制的内核方来说,就是需要实现Spi机制来加载插件提供者提供的插件,加载插件后进行什么操作,就看实际业务情况了。感兴趣的小伙伴,不妨尝试自己写一个简易的DriverManager以及ServiceLoader,来加载自己刚写的Sql驱动,仅需要一点文件操作和反射操作即可完成。

  1. 总结
    驱动的加载只是Spi功能的冰山一角,Spi机制的存在,能让业务代码影响较小的情况下,灵活的替换业务流程中的各种功能。目前典型的开源项目有:DubboDubbo框架中大量应用了Spi机制,使得开发者能够轻松替换Dubbo中的各种功能,例如日志、rpc访问等。