Java SPI机制的原理和实践

SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。

一、概述

Java SPI是SPI的一种重要实现方式,是JDK内置的一种服务发现方法,实现逻辑是:调用方通过调用JDK提供的标准化的服务接口,通过本地服务发现,加载第三方或者本地实现了该接口的类,通过这种方式,服务规范制定者制定接口规范,服务提供者按照接口进行实现。在JDK中实现数据库驱动按需加载就是利用SPI的方式实现的,JDK规定了java.sql.Driver接口,其具体实现可以是MySQL或者PostgreSQL,具体实现是第三方的驱动服务方提供,通过SPI机制加载供调用方使用。

1.应用场景

SPI机制应用在服务的规范制定者和服务的实现者需要分离的情况,就是接口中方法的规定和实现是分离的,这种机制经常在可选组件和可选插件的场景中使用,通过SPI的机制将具体的实现加载进来。具体的应用在 Java 的 java.util.spi package 中就约定了很多 SPI 接口。下面,列举一些 SPI 接口:

  1. TimeZoneNameProvider: 为 TimeZone 类提供本地化的时区名称。
  2. DateFormatProvider: 为指定的语言环境提供日期和时间格式。
  3. NumberFormatProvider: 为 NumberFormat 类提供货币、整数和百分比值。
  4. Driver: 从 4.0 版开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法加载驱动程序。
  5. PersistenceProvider: 提供 JPA API 的实现。

2.优点和缺点

优点:

SPI机制的优点是灵活性高,可以通过简单地添加或替换实现类来扩展应用程序的功能。同时,SPI机制也具有一定的可扩展性和可维护性,因为它将应用程序和具体实现解耦,实现了高内聚、低耦合的目标。

缺点:

SPI机制的缺点是需要程序员手动编写实现类并在META-INF/services目录下创建配置文件,这样会增加代码量和工作量。同时,SPI机制也存在安全风险,因为实现类是由外部提供的,可能存在恶意实现类的风险。

二、原理

1.主要组成和原理说明

要通过SPI实现动态服务发现,首先需要了解其主要的组成部分:

  1. **SPI 接口:**为服务提供者实现类约定的的接口或抽象类。
  2. **SPI 实现类:**实际提供服务的实现类。
  3. **SPI 配置:**Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
  4. **ServiceLoader:**Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

SPI原理说明

实现SPI的动态加载实现类主要是通过ServiceLoader类来实现。首先调用方通过ServiceLoader.load ()静态方法来加载SPI服务,ServiceLoader的成员变量中保存了实现类配置文件位置前缀、实现类信息、实现类ClassLoader信息、实现类实例信息等。

arduino 复制代码
//实现类配置文件位置前缀
private static final String PREFIX = "META-INF/services/";

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

ServiceLoader.load ()方法中调用构造方法ServiceLoader(),构造方法中调用reload() 方法,通过lookupIterator = new LazyIterator(service, loader);来初始化懒加载的迭代器,查看其next()方法,其核心就是从META-INF/services/ 目录中读取文件,将其转换为Class类对象加载进来。最后Class类对象通过newInstance()方法实例化,这样就根据SPI中的接口信息加载了实现类。

java 复制代码
//ServiceLoader.java

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

//将实现类的全限定类名转换为完整路径
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    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;
        }
java 复制代码
//Class.java

//根据类对象信息生成对象实例
public T newInstance()
    throws InstantiationException, IllegalAccessException
{
    if (System.getSecurityManager() != null) {
        checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
    }

    // NOTE: the following code may not be strictly correct under
    // the current Java memory model.

    // Constructor lookup
    if (cachedConstructor == null) {
        if (this == Class.class) {
            throw new IllegalAccessException(
                "Can not call newInstance() on the Class for java.lang.Class"
            );
        }
        try {
            Class<?>[] empty = {};
            final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
            // Disable accessibility checks on the constructor
            // since we have to do the security check here anyway
            // (the stack depth is wrong for the Constructor's
            // security check to work)
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<Void>() {
                    public Void run() {
                            c.setAccessible(true);
                            return null;
                        }
                    });
            cachedConstructor = c;
        } catch (NoSuchMethodException e) {
            throw (InstantiationException)
                new InstantiationException(getName()).initCause(e);
        }
    }
    Constructor<T> tmpConstructor = cachedConstructor;
    // Security check (same as in java.lang.reflect.Constructor)
    int modifiers = tmpConstructor.getModifiers();
    if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
        Class<?> caller = Reflection.getCallerClass();
        if (newInstanceCallerCache != caller) {
            Reflection.ensureMemberAccess(caller, this, null, modifiers);
            newInstanceCallerCache = caller;
        }
    }
    // Run constructor
    try {
        return tmpConstructor.newInstance((Object[])null);
    } catch (InvocationTargetException e) {
        Unsafe.getUnsafe().throwException(e.getTargetException());
        // Not reached
        return null;
    }
}

在代码阅读过程中,有点需要注意,在调用ServiceLoader.load ()方法时,会进行一次重载调用,会多传入了一个新的ClassLoader,这个ClassLoader是ApplicationClassLoader,他的作用是加载目前运行中应用的类对象,需要这样做的原因是JDK的双亲委派机制决定的,即加载ServiceLoader类的ClassLoader是BootstrapClassLoader,所以默认情况通过他去创建的对象也是BootstrapClassLoader,但通过SPI机制需要加载的实现类都在classpath中,无法被加载,所以通过Thread.currentThread().getContextClassLoader();这个方法来获取ApplicationClassLoader。

java 复制代码
//ServiceLoader.java

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
java 复制代码
//Thread.java

public ClassLoader getContextClassLoader() {
    if (contextClassLoader == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                               Reflection.getCallerClass());
    }
    return contextClassLoader;
}

2.SPI在 JDBC DriverManager上的应用案例分析

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。

具体的使用方法是:

  1. JDBC接口定义 :首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
  2. MySQL实现 :在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
  3. 服务使用:通过以下方法就能直接使用
ini 复制代码
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);

原理分析

DriverManager.getConnection(url,username,password);执行的代码逻辑。

java 复制代码
//DriverManager.java

//通过用户名密码等信息获取连接
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()));
}



//通过实例化后的驱动连接数据库
    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(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");
    }

DriverManager.java的静态代码段执行loadInitialDrivers();方法,该方法的主要逻辑为:

  1. 从系统变量中获取有关驱动的定义。
  2. 使用SPI来获取驱动的实现。
  3. 遍历使用SPI获取到的具体实现,实例化各个实现类。
  4. 根据第一步获取到的驱动列表来实例化具体实现类。
java 复制代码
//DriverManager.java

//静态代码段
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}


//根据配置信息加载对于的数据库驱动
    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                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<Void>() {
            public Void run() {

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

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                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);
            }
        }
    }

通过以上方式,就能把实现Driver接口的驱动实现类加载进来了。

SPI在slf4j上应用的案例分析

todo

三、实践

1.实现步骤

  1. 定义接口:定义一个接口,声明一些抽象方法。
  2. 创建实现类:创建一个或多个实现该接口的类。
  3. 配置文件:在META-INF/services/目录下创建一个以接口全限定名为命名的文件,内容为实现类的全限定名,每行一个。
  4. 加载配置:使用ServiceLoader类加载配置文件并解析出实现类。

2.代码实现

实现通过SPI加载不同数据库实现方式的功能,数据库的实现方式包括Mysql和redis。

(1)实现接口

java 复制代码
public interface DataStorage {
    String search(String key);
}

(2)创建实现类

创建2个实现类,分别是Mysql和redis的实现方式。

java 复制代码
public class MysqlStorage implements DataStorage{

    @Override
    public String search(String key) {
        return "【Mysql】搜索" + key + ",结果:No";
    }
}
java 复制代码
public class RedisStorage implements DataStorage{

    @Override
    public String search(String key) {
        return "【Redis】搜索" + key + ",结果:Yes";
    }
}

(3)配置文件

(4)加载配置

打印出通过SPI加载到的数据库驱动。

java 复制代码
import java.util.ServiceLoader;

public class SpiDemo {
    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("============ Java SPI 测试============");
        serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));
    }

}

(5)验证效果

四、总结

(1)SPI的核心思路是什么?

SPI源码的代码逻辑还是比较复杂,但是核心的思路就是通过ApplicationClassloader加载到配置好的实现类,获取ApplicationClassloader的方法是getContextClassLoader()方法,配置类信息记录在约定好的META-INF/services/目录中。

(2)SPI模式和API模式的区别?

两种模式都区分服务提供者和服务调用者的,API的服务实现放在服务提供者方那边,和接口一起提供;SPI的服务实现可以第三方实现或者自己实现,和接口是分开的。


TODO

  • 补充SPI在slf4j上应用的案例分析;
  • 增加Java、Spring、Dubbo三者SPI机制区别的总结;

参考资料

  1. 鲜为人知的Java SPI机制:juejin.cn/post/722475...
  2. Java常用机制 - SPI机制详解:www.pdai.tech/md/java/adv...
  3. 源码级深度理解 Java SPI:zhuanlan.zhihu.com/p/580004065...
  4. 阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别:juejin.cn/post/721060...

本文由博客一文多发平台 OpenWrite 发布!

相关推荐
无相求码19 分钟前
为什么你的第二个 scanf 还没输入就直接跳过了?C语言输入缓冲区陷阱
c语言·后端
叼馒女友郭芙蓉19 分钟前
Function Calling理解
后端
玉宇夕落20 分钟前
LangChain RAG Retriever 的学习
后端
用户2986985301426 分钟前
Python 一键导出 Excel 中的图表和形状为图片
后端·python·excel
用户81300778163727 分钟前
PageHelper分页查询步骤
后端
是小李呀30 分钟前
MyBatis、MyBatis-Plus 与全自动 ORM 的本质区别
后端
小堂子这厢有礼了43 分钟前
Chet.Admin 权限模型:菜单级 + 按钮级 + 行级三层防护
前端·后端·信息可视化·前端框架·.net
SimonKing43 分钟前
Spring Boot 4.0 + gRPC 极简集成:4步搞定,比REST还丝滑
java·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,让我加班到凌晨三点
前端·人工智能·后端
Wang's Blog2 小时前
Go-Zero基础入门5: 探究go-zero如何基于gRPC扩展客户端
开发语言·后端·golang·go-zero