本文介绍 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 加载和实例化。该注解有两个方法:
@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(成员属性):默认名称,用于缓存默认的实现类别名。
核心方法:
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
// 注意,这里每个 SPI 接口的类加载器都是独立的(类加载隔离)
return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
}
复制代码
不同的 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
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 编排,能够在保留性能的前提下,更好的提告网关本身的可扩展性。
更多详情查看微信公众号
评论