什么是SPI机制?

1 SPI(Service Provider Interface)

服务提供者接口的意思

简单理解:SPI机制为一种服务扩展机制,首先在配置文件中定义好接口的实现类,然后根据这个接口从配置文件中加载该接口的所有实现类,以供使用。

开发常见示例

  • JDBC驱动加载:根据不同的数据库厂商加载不同的JDBC驱动包

  • SpringBoot的SPI机制:在META-INF下的spring.factories中加上自定义的自动配置,事件监听器或初始化器等

  • Dubbo的SPI机制:如路由扩展,负载均衡扩展,集群扩展等

 

2 示例说明如何使用SPI机制

自定义一个接口

public interface DriverInterface {
    public void hello();
}

编写两个测试的接口实现类

public class MyDriver implements DriverInterface {
    @Override
    public void hello() {
        System.out.println("My driver 1");
    }
}
public class MyDriver2 implements DriverInterface {
    @Override
    public void hello() {
        System.out.println("My driver 2");
    }
}

在resource下创建META-INF/services文件夹

并创建以接口的全限定名命名的文件

//内容包含该接口具体实现类的全限定名,以下为文件内容com.coolcoding.boot.spi.MyDrivercom.coolcoding.boot.spi.MyDriver2

测试SPI

public class SpiClient {
​
    public static void main(String[] args) {
    // 使用java.util.ServiceLoader 根据接口.class来加载
        ServiceLoader load = ServiceLoader.load(DriverInterface.class);
        //输出测试
        load.forEach(DriverInterface::hello);
    }
}

测试结果如下:顺利打印两个实现类具体方法的处理

My driver 1
My driver 2

3 ServiceLoader源码分析

从上一步的SpiClientServiceLoader.load(DriverInterface.class);从这一步进入分析是如何加载的

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

new 一个serviceLoader

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

构造器中初始化一些变量,然后执行reload()

private ServiceLoader(Class 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();
}

reload():创建一个LazyIterator

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}
//并初始化lazyIterator的变量属性
private LazyIterator(Class service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

如何去加载之前创建的META-INF/services文件夹中的实现类?

LazyIterator:懒加载迭代器,可以从名字中注意到在迭代器迭代的时候去加载指定接口的实现类,实现懒加载!

查看LazyIterator中的迭代方法

public boolean hasNext() {
    if (acc == null) {
    //进入hasNextService()方法
        return hasNextService();
    } else {
        PrivilegedAction action = new PrivilegedAction() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

hasNextService()

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
        //PREFIX : private static final String PREFIX = "META-INF/services/";
        //service.getName() 即接口的全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
            //加载META-INF/services下的接口文件中的服务实现类
                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;
}

结论

至此调LazyIterator的hasNextService方法时最终将去META-INF/services/目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator的成员变量nextName。

 

思考:为何在此处使用懒加载?

查看类似迭代器在while()中的处理,执行完hasNextService之后会执行nextService()方法

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.iterator方法真正承担了加载,使用反射方式实例化META-INF/services/目录下的接口文件里定义的服务提供者实现类

 

4 实际案例源码分析

典型示例:JDBC驱动加载

查看java的核心库(rt.jar)有java.sql.Driver接口和java.sql.DriverManager

Driver:各个数据库厂商的驱动类要实现的接口

什么是SPI机制?_第1张图片

 

DriverManager:用来管理数据库的驱动类的,值得注意的是DriverManager这个类有一个registeredDrivers集合属性,用来存储Mysql的驱动类

什么是SPI机制?_第2张图片

 

如引入MySQL,则mysql的驱动包有com.mysql.jdbc.Driver类

public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
    }
​
    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

查看com.mysql.cj.jdbc.Driver源码

实现了java的核心库(rt.jar)有java.sql.Driver接口,该步骤则契合了最开始的示例中实现类需要实现指定的接口

//实现了implements java.sql.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
​
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}
在JDBC编程中我们需要通过DriverManager获取一个连接,而DriverManager有静态代码块,则会先执行DriverManager中的静态代码块
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");

DriverManager的静态代码块

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

loadInitialDrivers():加载初始化驱动器

其中同样使用了ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);则跟前面的测试SPI相契合

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
​
        AccessController.doPrivileged(new PrivilegedAction() {
            public Void run() {
//该处熟悉的地方:采用ServiceLoader加载Driver接口的具体实现类,即数据库驱动实现类
                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
​
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
​
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

结论

Mysql的驱动类加载主要也是利用JAVA的SPI机制,即ServiceLoader来实现加载并实例化Mysql的驱动类

 

在什么时候Mysql的驱动类被注册到DriverManager的registeredDriver集合中?

com.mysql.jdbc.Driver也有个静态代码块

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
​
    static {
        try {
        //注册驱动类
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

DriverManager.registerDriver():

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 {
​
    /* Register the driver if it has not already been added to our list */
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        // This is for compatibility with the original DriverManager
        throw new NullPointerException();
    }
​
    println("registerDriver: " + driver);
​
}

结论

在Driver中的静态代码块中会往DriverManager中注册驱动类。

 

驱动类在哪里被使用到?

从DriverManager获取连接中可以分析获取连接需要加载驱动类

connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456");
 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 {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
​
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
​
        println("DriverManager.getConnection(\"" + url + "\")");
​
        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;
//************此处for循环从registeredDrivers list中获取到之前加载的驱动类
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            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());
            }
​
        }
​
        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }
​
        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

结论

至此分析了驱动类如何去加载以及在哪一步中使用到了驱动类。

在Dubbo中也有采用SPI机制,如其中的源码分析中Protocol下有DubboProtocol和RegistryProtocol,这两个类在Dubbo中的服务暴露和服务引用中起着很关键的作用,详细可以查看Dubbo官网源码分析

你可能感兴趣的:(计算机,SPI机制,spi,java,spring)