写点什么

OHara Gateway SPI 动态加载机制图解

作者:路 飞
  • 2025-03-06
    浙江
  • 本文字数:4877 字

    阅读完需:约 16 分钟

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 配置文件中列出其扩展实现类的全限定名:

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

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

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 对象。

// 获取 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);
复制代码

跟进下该方法源码:

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。

@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 {
/** * 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 <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {    // 注意,这里每个 SPI 接口的类加载器都是独立的(类加载隔离)    return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());}
复制代码


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

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

SpiExtensionFactory

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

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。

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


@Joinpublic class MysqlSPI implements JdbcSPI {    @Override    public String getClassName() {        return "mysql";    }}
复制代码


@Joinpublic class OracleSPI implements JdbcSPI {    @Override    public String getClassName() {        return "oracle";    }}
复制代码

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


mysql=org.ohara.spi.extend.case1.MysqlSPIoracle=org.ohara.spi.extend.case1.OracleSPI
复制代码

单元测试类:

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 编排,能够在保留性能的前提下,更好的提告网关本身的可扩展性。


更多详情查看微信公众号


发布于: 刚刚阅读数: 5
用户头像

路 飞

关注

还未添加个人签名 2022-07-12 加入

还未添加个人简介

评论

发布
暂无评论
OHara Gateway SPI动态加载机制图解_路 飞_InfoQ写作社区