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扩展1实现类都会被 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") // 默认扩展实现类是 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

image.png

mysql=org.ohara.spi.extend.case1.MysqlSPI
oracle=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 编排,能够在保留性能的前提下,更好的提告网关本身的可扩展性。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路 飞

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值