Java SPI机制的原理和实践

2023年 9月 12日 59.3k 0

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

image-20230808213108346

一、概述

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

img

1.应用场景

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

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

    优点:

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

    缺点:

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

    二、原理

    1.主要组成和原理说明

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

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

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

    //实现类配置文件位置前缀
    private static final String PREFIX = "META-INF/services/";
    
    // The class or interface representing the service being loaded
    private final Class 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 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中的接口信息加载了实现类。

    //ServiceLoader.java
    
    //静态方法load()
    public static  ServiceLoader load(Class 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;
            }
    
    //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 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() {
                        public Void run() {
                                c.setAccessible(true);
                                return null;
                            }
                        });
                cachedConstructor = c;
            } catch (NoSuchMethodException e) {
                throw (InstantiationException)
                    new InstantiationException(getName()).initCause(e);
            }
        }
        Constructor 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。

    //ServiceLoader.java
    
    public static  ServiceLoader load(Class service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
    //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扩展机制来实现。

    具体的使用方法是:

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

    原理分析

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

    //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();方法,该方法的主要逻辑为:

  • 从系统变量中获取有关驱动的定义。
  • 使用SPI来获取驱动的实现。
  • 遍历使用SPI获取到的具体实现,实例化各个实现类。
  • 根据第一步获取到的驱动列表来实例化具体实现类。
  • //DriverManager.java
    
    //静态代码段
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    
    //根据配置信息加载对于的数据库驱动
        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 loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator 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.实现步骤

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

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

    (1)实现接口

    public interface DataStorage {
        String search(String key);
    }
    

    (2)创建实现类

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

    public class MysqlStorage implements DataStorage{
    
        @Override
        public String search(String key) {
            return "【Mysql】搜索" + key + ",结果:No";
        }
    }
    
    public class RedisStorage implements DataStorage{
    
        @Override
        public String search(String key) {
            return "【Redis】搜索" + key + ",结果:Yes";
        }
    }
    

    (3)配置文件

    image-20230808205129264

    (4)加载配置

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

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

    (5)验证效果

    image-20230808205020924

    四、总结

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

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

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

    TODO

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

    参考资料

  • 鲜为人知的Java SPI机制:juejin.cn/post/722475…
  • Java常用机制 - SPI机制详解:www.pdai.tech/md/java/adv…
  • 源码级深度理解 Java SPI:zhuanlan.zhihu.com/p/580004065…
  • 阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别:juejin.cn/post/721060…
  • 本文由博客一文多发平台 OpenWrite 发布!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论