OHara Gateway SPI动态加载机制图解

本文介绍 OHara Gateway 中的 SPI 扩展机制,参自考 Dubbo SPI 实现原理 @see:Apache Dubbo

OHara Gateway 中很多核心模块都依赖 SPI 机制去动态加载对应的扩展实现类。例如:操作符匹配策略(MatchStrategy)、多种缓存插件加载(ICacheBuilder)、多种负载均衡策略加载(LoadBalancer) 等等。

一、SPI与API的区别?

  • SPI:即 Service Provider Interface,是一种动态服务发现机制。可以在系统运行时动态加载接口实现(选择性加载、懒加载),非常适用于插件编排、可拔插架构场景,可扩展较强。支持开发者在不修改核心代码的情况下增加新的接口实现。

  • API:即 Application Programming Interface 应用程序编程接口。由服务提供方预先定义接口协议、参数类型等,允许第三方软件系统通过该接口进行数据交换。例如跨平台或跨系统服务调用、微服务间通信、开放平台(阿里云开放平台、高德开放平台、菜鸟开放平台)等。

二、与JDK内置SPI的区别?

JDK SPI

JDK内置SPI机制需要扩展类具备如下几个条件:

  • 有空参构造函数(用于实例化扩展类)不支持依赖注入。

  • 必须要在 META-INF/services 路径下定义配置文件,文件名为接口全限定名(例如,java.sql.Driver),文件内容是具体实现类的全限定名。

  • 通过 java.util.ServiceLoader 加载。

java.sql.Driver 为例, 就是 JDBC(Java Database Connectivity)规范中定义的一个 SPI 接口,用于与数据库进行交互。MySQL 和 Oracle 等数据库厂商通过实现这个接口来提供自己的 JDBC 驱动程序,使得Java 应用可以连接、查询和操作这些数据库。

看下代码详情:

在 META-INF/services/java.sql.Driver 配置文件中列出其扩展实现类的全限定名:

bash 复制代码
# MySQL厂商实现JDBC驱动
com.mysql.cj.jdbc.Driver

所有的 java.sql.Driver扩展实现类都会被 DriverManager 管理。

java 复制代码
public class DriverManager {

    ...省略

    // List of registered JDBC drivers
    private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    public static void registerDriver(java.sql.Driver driver) throws SQLException {
        registerDriver(driver, null);
    }

    public static 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.getConnection() 方法获取数据库连接。DriverManager 会根据提供的 URL用户名密码选择合适的驱动程序,并返回一个 Connection 对象。

ini 复制代码
// 获取 MySQL 数据库连接
String url = "jdbc:mysql://localhost:3306/dbname";
String user = "root";
String password = "password";
Connection conn = DriverManager.getConnection(url, user, password);

// 获取 Oracle 数据库连接
String oracleUrl = "jdbc:oracle:thin:@localhost:1521:orcl";
String oracleUser = "USER";
String oraclePassword = "PASSWORD";
Connection oracleConn = DriverManager.getConnection(oracleUrl, oracleUser, oraclePassword);

跟进下该方法源码:

java 复制代码
public class DriverManager {

    ...省略
    
        @CallerSensitive
    public static Connection getConnection(String url)
        throws SQLException {

        java.util.Properties info = new java.util.Properties();
        return (getConnection(url, info, Reflection.getCallerClass()));
    }
    
    //  Worker method called by the public getConnection() methods.
    @CallerSensitiveAdapter
    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;
        if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }

        if (url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // 继续看这个核心方法!
        ensureDriversInitialized();

        // 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) {
                ...省略
        }
            
    }

    ...省略
}

OHara SPI

OHara SPI 机制是在 JDK SPI 基础上做了一些扩展,例如:

  • 自定义的类加载器管理机制,确保每个扩展点的实现类都能正确加载且互不干扰。

  • 支持依赖注入,可以通过注解或配置文件为扩展类注入依赖。

  • 提供了更完善的缓存机制,确保已加载的服务提供者不会重复加载,并且可以安全地清理不再使用的提供者,避免内存泄漏。

  • 允许自定义扩展类的加载顺序,优先级/权重。

  • 支持懒加载,选择性加载。

@SPI注解

SPI 标识注解,使用该注解的接口才能被 OHara SPI 机制加载。只有一个value()方法,表示默认扩展实现类的名称key。

less 复制代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {

    /**
     * 默认扩展实现名.
     *
     * @return the string
     */
    String value() default "";
}

@JOIN注解

@JOIN 需要用在 @SPI 接口实现类上,用于被 ExtensionLoader 加载和实例化。该注解有两个方法:

  • order()用于扩展类加载排序。

  • isSingleton()该扩展类需要以单例模式加载,默认为单例。

    @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Join {

    vbnet 复制代码
    /**
     * It will be sorted according to the current serial number.
     *
     * @return int.
     */
    int order() default 0;
    
    
    /**
     * Indicates that the object joined by @Join is a singleton,
     * otherwise a completely new instance is created each time.
     *
     * @return true or false.
     */
    boolean isSingleton() default true;

    }

ExtensionLoader

核心参数:

  • LOADERS:全局静态缓存,用于缓存已经加载的 ExtensionLoader 实例。

  • OHARA_DIRECTORY:定义了 SPI 配置文件所在的相对目录

  • HOLDER_COMPARATOR 和 CLASS_ENTITY_COMPARATOR:两个比较器,用于根据序号对 Holder 和 ClassEntity 进行排序。

  • clazz(成员属性):表示当前 ExtensionLoader 管理的接口类型。

  • classLoader(成员属性):类加载器,用于加载实现类。

  • cachedClasses(成员属性):已加载类缓存,缓存了所有已加载的实现类信息。

  • cachedInstances(成员属性):已加载实例Horder缓存,缓存了所有已加载的实现类实例的Horder持有器。

  • joinInstances(成员属性):单实例缓存,缓存了单例模式的实现类实例。

  • cachedDefaultName(成员属性):默认名称,用于缓存默认的实现类别名。

核心方法:

  • <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) 方法:从当前 ExtensionLoader 实例中获取目标类型的 SPI 扩展类集合。

    public static ExtensionLoader getExtensionLoader(final Class clazz) { // 注意,这里每个 SPI 接口的类加载器都是独立的(类加载隔离) return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader()); }

  • T getJoin(final String name)方法:根据指定别名,获取其对象实例,支持单例模式和非单例模式。

不同的SPI接口 之间类加载器隔离图解:

SpiExtensionFactory

该工厂类主要是对上面 ExtensionLoader 的二次包装,可忽略。

arduino 复制代码
public class SpiExtensionFactory {
    // 获取目标SPI接口的默认扩展实现类
    public static <T> T getDefaultExtension(final Class<T> clazz) {
        return Optional.ofNullable(clazz)
                // 入参clazz必须是接口
                .filter(Class::isInterface)
                // 入参clazz必须被@SPI标识
                .filter(cls -> cls.isAnnotationPresent(SPI.class))
                // 基于clazz这个接口类型实例化ExtensionLoader
                .map(ExtensionLoader::getExtensionLoader)
                // 获取该@SPI标识接口的默认实现,不存在则返回NULL
                .map(ExtensionLoader::getDefaultJoin)
                .orElse(null);
    }

    // 获取目标SPI接口的指定扩展实现类
    public static <T> T getExtension(String key, final Class<T> clazz) {
        return ExtensionLoader.getExtensionLoader(clazz).getJoin(key);
    }

    // 获取目标SPI接口的所有扩展实现类
    public static <T> List<T> getExtensions(final Class<T> clazz) {
        return ExtensionLoader.getExtensionLoader(clazz).getJoins();
    }
}

使用案例

以一组单元测试为例,定一个 JdbcSPI ,该接口有两个 SPI 扩展实现类 MysqlSPI、OracleSPI。

typescript 复制代码
@SPI(value = "mysql") // 默认扩展实现类是 MysqlSPI
public interface JdbcSPI {
    String getClassName();
}

@Join
public class MysqlSPI implements JdbcSPI {
    @Override
    public String getClassName() {
        return "mysql";
    }
}

@Join
public class OracleSPI implements JdbcSPI {
    @Override
    public String getClassName() {
        return "oracle";
    }
}

/META-INFO/ohara 目录文件下存放扩展配置文件:org.ohara.spi.extend.case1.JdbcSPI

ini 复制代码
mysql=org.ohara.spi.extend.case1.MysqlSPI
oracle=org.ohara.spi.extend.case1.OracleSPI

单元测试类:

ini 复制代码
class ExtensionLoaderTest {

    @Test
    void test_getExtensionLoader() {
        ExtensionLoader<ListSPI> listExtensionLoader = ExtensionLoader.getExtensionLoader(ListSPI.class);
        List<ListSPI> joins = listExtensionLoader.getJoins();
        assertEquals(2, joins.size());

        JdbcSPI mysql = ExtensionLoader.getExtensionLoader(JdbcSPI.class).getJoin("mysql");
        assertInstanceOf(MysqlSPI.class, mysql);
        JdbcSPI defaultJdbc = SpiExtensionFactory.getDefaultExtension(JdbcSPI.class);
        assertInstanceOf(MysqlSPI.class, defaultJdbc);
        JdbcSPI oracle = SpiExtensionFactory.getExtension("oracle", JdbcSPI.class);
        assertInstanceOf(OracleSPI.class, oracle);
    }

}

三、总结

OHara Gateway 的 SPI 机制相比 JDK 内置的 SPI 机制更加灵活和强大,特别是在扩展点隔离、懒加载、依赖注入、复杂配置支持等方面。通过 OHara SPI,可以更高效地管理和扩展系统功能,提升系统的性能、稳定性和可维护性。与传统的MVC架构和DDD业务架构不同,OHara Gateway 通过 SPI 机制 + Plugin 编排,能够在保留性能的前提下,更好的提告网关本身的可扩展性。

相关推荐
秋野酱14 分钟前
基于javaweb的SpringBoot自习室预约系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
weloveut20 分钟前
西门子WinCC Unified PC的GraphQL使用手册
后端·python·graphql
面试官E先生34 分钟前
【极兔快递Java社招】一面复盘|数据库+线程池+AQS+中间件面面俱到
java·面试
琢磨先生David38 分钟前
构建优雅对象的艺术:Java 建造者模式的架构解析与工程实践
java·设计模式·建造者模式
小雅痞1 小时前
[Java][Leetcode simple]26. 删除有序数组中的重复项
java·leetcode
青云交1 小时前
Java 大视界 -- 基于 Java 的大数据分布式存储在工业互联网海量设备数据长期存储中的应用优化(248)
java·大数据·工业互联网·分布式存储·冷热数据管理·hbase 优化·kudu 应用
纸包鱼最好吃1 小时前
java基础-package关键字、MVC、import关键字
java·开发语言·mvc
唐山柳林1 小时前
城市生命线综合管控系统解决方案-守护城市生命线安全
java·安全·servlet
PgSheep1 小时前
Spring Cloud Gateway 聚合 Swagger 文档:一站式API管理解决方案
java·开发语言
蒂法就是我2 小时前
详细说说Spring的IOC机制
java·后端·spring