写点什么

🏆【Java 技术之旅】带你深入理解和认识 SPI 运作机制

发布于: 2021 年 07 月 31 日
🏆【Java 技术之旅】带你深入理解和认识SPI运作机制

什么是 SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。


这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。我们先通过一个很简单的例子来看下它是怎么用的。

小栗子

  • 首先,我们需要定义一个接口,SPIService


public interface SPIService {    void execute();}
复制代码


  • 然后,定义两个实现类,没别的意思,只输入一句话。


public class SpiImpl1 implements SPIService{    public void execute() {        System.out.println("SpiImpl1.execute()");    }}

public class SpiImpl2 implements SPIService{ public void execute() { System.out.println("SpiImpl2.execute()"); }}
复制代码


  • 最后呢,要在 ClassPath 路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。

文件路径如下:

com.xxx.SPIService
复制代码


SPI 配置文件位置


src/main/resource/META-INF/services/com.xxx.SPIService
复制代码


  • 内容就是实现类的全限定类名:


com.viewscenes.netsupervisor.spi.SpiImpl1com.viewscenes.netsupervisor.spi.SpiImpl2
复制代码

测试

然后我们就可以通过 ServiceLoader.load 或者 Service.providers 方法拿到实现类的实例。其中,Service.providers 包位于 sun.misc.Service,而 ServiceLoader.load 包位于 java.util.ServiceLoader。


public class Test {    public static void main(String[] args) {        Iterator<SPIService> providers = Service.providers(SPIService.class);        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);        while(providers.hasNext()) {            SPIService ser = providers.next();            ser.execute();        }        System.out.println("--------------------------------");        Iterator<SPIService> iterator = load.iterator();        while(iterator.hasNext()) {            SPIService ser = iterator.next();            ser.execute();        }    }
复制代码


  • 两种方式的输出结果是一致的:


SpiImpl1.execute()SpiImpl2.execute()--------------------------------SpiImpl1.execute()SpiImpl2.execute()
复制代码

源码分析

看到一个位于 sun.misc 包,一个位于 java.util 包,sun 包下的源码看不到。我们就以 ServiceLoader.load 为例,通过源码看看它里面到底怎么做的。

ServiceLoader

首先,我们先来了解下 ServiceLoader,看看它的类结构。


public final class ServiceLoader<S> implements Iterable<S>    //配置文件的路径    private static final String PREFIX = "META-INF/services/";    //加载的服务类或接口    private final Class<S> service;    //已加载的服务类集合    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();    //类加载器    private final ClassLoader loader;    //内部类,真正加载服务类    private LazyIterator lookupIterator;}
复制代码

Load

load 方法创建了一些属性,重要的是实例化了内部类,LazyIterator。最后返回 ServiceLoader 的实例。


public final class ServiceLoader<S> implements Iterable<S>    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;        //先清空        providers.clear();        //实例化内部类         LazyIterator lookupIterator = new LazyIterator(service, loader);    }}
复制代码

查找实现类

查找实现类和创建实现类的过程,都在 LazyIterator 完成。当我们调用 iterator.hasNext 和 iterator.next 方法的时候,实际上调用的都是 LazyIterator 的相应方法。


public Iterator<S> iterator() {    return new Iterator<S>() {        public boolean hasNext() {            return lookupIterator.hasNext();        }        public S next() {            return lookupIterator.next();        }        .......    };}
复制代码


所以,我们重点关注 lookupIterator.hasNext()方法,它最终会调用到 hasNextService。


private class LazyIterator implements Iterator<S>{    Class<S> service;    ClassLoader loader;    Enumeration<URL> configs = null;    Iterator<String> pending = null;    String nextName = null;     private boolean hasNextService() {        //第二次调用的时候,已经解析完成了,直接返回        if (nextName != null) {            return true;        }        if (configs == null) {            //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件            //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService            String fullName = PREFIX + service.getName();            //将文件路径转成URL对象            configs = loader.getResources(fullName);        }        while ((pending == null) || !pending.hasNext()) {            //解析URL文件对象,读取内容,最后返回            pending = parse(service, configs.nextElement());        }        //拿到第一个实现类的类名        nextName = pending.next();        return true;    }}
复制代码

创建实例

当然,调用 next 方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。


private class LazyIterator implements Iterator<S>{    private S nextService() {        //全限定类名        String cn = nextName;        nextName = null;        //创建类的Class对象        Class<?> c = Class.forName(cn, false, loader);        //通过newInstance实例化        S p = service.cast(c.newInstance());        //放入集合,返回实例        providers.put(cn, p);        return p;    }}
复制代码

JDBC 中的应用

SPI 机制为很多框架的扩展提供了可能,其实 JDBC 就应用到了这一机制。回忆一下 JDBC 获取数据库连接的过程。在早期版本中,需要先设置数据库驱动的连接,再通过DriverManager.getConnection获取一个 Connection。


String url = "jdbc:mysql:///consult?serverTimezone=UTC";String user = "root";String password = "root";Class.forName("com.mysql.jdbc.Driver");Connection connection = DriverManager.getConnection(url, user, password);
复制代码

加载

DriverManager 类,它在静态代码块里面做了一件比较重要的事。很明显,它已经通过 SPI 机制, 把数据库驱动连接初始化了。


public class DriverManager {    static {        loadInitialDrivers();        println("JDBC DriverManager initialized");    }}
复制代码


具体过程还得看 loadInitialDrivers,它在里面查找的是 Driver 接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver


public class DriverManager {    private static void loadInitialDrivers() {        AccessController.doPrivileged(new PrivilegedAction<Void>() {            public Void run() {                //很明显,它要加载Driver接口的服务类,Driver接口的包为:java.sql.Driver                //所以它要找的就是META-INF/services/java.sql.Driver文件                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);                Iterator<Driver> driversIterator = loadedDrivers.iterator();                try{                    //查到之后创建对象                    while(driversIterator.hasNext()) {                        driversIterator.next();                    }                } catch(Throwable t) {                    // Do nothing                }                return null;            }        });    }}
复制代码


我们来看 MySQL 的 jar 包,就是这个文件,文件内容为:com.mysql.cj.jdbc.Driver

创建实例

上一步已经找到了 MySQL 中的 com.mysql.cj.jdbc.Driver 全限定类名,当调用 next 方法时,就会创建这个类的实例。它就完成了一件事,向 DriverManager 注册自身的实例。


public class Driver extends NonRegisteringDriver implements java.sql.Driver {    static {        try {            //注册            //调用DriverManager类的注册方法            //往registeredDrivers集合中加入实例            java.sql.DriverManager.registerDriver(new Driver());        } catch (SQLException E) {            throw new RuntimeException("Can't register driver!");        }    }    public Driver() throws SQLException {        // Required for Class.forName().newInstance()    }}
复制代码

创建 Connection

在 DriverManager.getConnection()方法就是创建连接的地方,它通过循环已注册的数据库驱动程序,调用其 connect 方法,获取连接并返回。


private static Connection getConnection(        String url, java.util.Properties info, Class<?> caller) throws SQLException {       //registeredDrivers中就包含com.mysql.cj.jdbc.Driver实例    for(DriverInfo aDriver : registeredDrivers) {        if(isDriverAllowed(aDriver.driver, callerCL)) {            try {                //调用connect方法创建连接                Connection con = aDriver.driver.connect(url, info);                if (con != null) {                    return (con);                }            }catch (SQLException ex) {                if (reason == null) {                    reason = ex;                }            }        } else {            println("    skipping: " + aDriver.getClass().getName());        }    }}
复制代码

再扩展

既然我们知道 JDBC 是这样创建数据库连接的,我们能不能再扩展一下呢?如果我们自己也创建一个 java.sql.Driver 文件,自定义实现类 MyDriver,那么,在获取连接的前后就可以动态修改一些信息。


还是先在项目 ClassPath 下创建文件,文件内容为自定义驱动类


com.viewscenes.netsupervisor.spi.MyDriver
复制代码

自定义数据库驱动程序

我们的 MyDriver 实现类,继承自 MySQL 中的 NonRegisteringDriver,还要实现 java.sql.Driver 接口。这样,在调用 connect 方法的时候,就会调用到此类,但实际创建的过程还靠 MySQL 完成。


public class MyDriver extends NonRegisteringDriver implements Driver{    static {        try {            java.sql.DriverManager.registerDriver(new MyDriver());        } catch (SQLException E) {            throw new RuntimeException("Can't register driver!");        }    }    public MyDriver()throws SQLException {}    public Connection connect(String url, Properties info) throws SQLException {      System.out.println("准备创建数据库连接.url:"+url);      System.out.println("JDBC配置信息:"+info);      info.setProperty("user", "root");      Connection connection =  super.connect(url, info);      System.out.println("数据库连接创建完成!"+connection.toString());      return connection;    }}
复制代码

输出结果

准备创建数据库连接.url:jdbc:mysql:///consult?serverTimezone=UTCJDBC配置信息:{user=root, password=root}数据库连接创建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f
复制代码


发布于: 2021 年 07 月 31 日阅读数: 6
用户头像

🏆2021年InfoQ写作平台-签约作者 🏆 2020.03.25 加入

👑【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 我们始于迷惘,终于更高水平的迷惘

评论

发布
暂无评论
🏆【Java 技术之旅】带你深入理解和认识SPI运作机制