手写RPC框架05-通过SPI机制增加框架的扩展性的设计与实现

news2025/6/19 17:24:55

源码地址:https://github.com/lhj502819/IRpc/tree/v6

系列文章:

  • 注册中心模块实现
  • 路由模块实现
  • 序列化模块实现
  • 过滤器模块实现
  • 自定义SPI机制增加框架的扩展性的设计与实现

现有的问题

在上一章节末尾我们提到了,目前我们的RPC框架可扩展性还不太友好,用户如果想自定义一个过滤器或者序列化方式还需要去修改源码。本次我们就通过SPI的机制去解决这个问题。

什么是SPI?

SPI全称Service Provider Interface,是Jdk提供的一种用来扩展框架的服务能力的机制,它能够在运行时将我们定义的类加载到JVM中并实例化。通常面向对象编程推荐的是面向接口编程,而SPI机制就需要先定义好接口,后续对接口进行实现,而如果我们想要替换实现或者增加接口实现的的话,一般都需要修改源代码,SPI机制就是来解决这个问题的,在运行时可以动态的去加载我们配置的Class,将其装配到框架中去。

常见的SPI实现

jdk原生

Jdk从1.6起引入了SPI机制,我们需要在指定目录META-INF/services下创建我们SPI的文件,文件名称为需要扩展的接口全限定名,如:cn.onenine.irpc.framework.core.router.IRouter,将自定义的实现类配置到里边,如: cn.onenine.irpc.framework.core.router.RandomRouterImpl,这样我们就可以使用Jdk的API去获取到我们自定义的类对象。

使用方式

可扩展接口定义

public interface ISpiTest {

    void doSomething();

}

自定义实现

public class DefaultISpiTest implements ISpiTest{
    @Override
    public void doSomething() {
        System.out.println("执行测试方法");
    }
}

SPI配置文件
在这里插入图片描述

集成代码

public static void main(String[] args) {
    ServiceLoader<ISpiTest> serviceLoader = ServiceLoader.load(ISpiTest.class);
    Iterator<ISpiTest> iSpiTestIterator = serviceLoader.iterator();
    while (iSpiTestIterator.hasNext()) {
        ISpiTest iSpiTest = iSpiTestIterator.next();
        TestSpiDemo.doTest(iSpiTest);
    }
}

实现原理

Jdk的SPI会在执行iterator#hasNext的时候去加载相关的类信息
在这里插入图片描述

读取到我们定义的文件后,会将文件内容读取出来,将Class的全限定名保存,在调用Iterator#next时才会创建类对象。
在这里插入图片描述

实际应用

我们在使用原生MySQL的JDBC的时候,都知道有个API叫DriverManager,它就是通过SPI的方式去加载Jdk提供的java.sql.Driver实现类,具体的配置如下,我使用的8.0驱动,其他版本的可能会有些许不同。
在这里插入图片描述

DriverManager中有静态代码块去加载对应的类实例
在这里插入图片描述

在这里插入图片描述

最终jdbc Driver在初始化时会将自身注册到DriverManager中,供DriverManager#getConnection使用。
在这里插入图片描述

缺点

  • 加载实现的时候是通过迭代器把所有配置的实现都加在一遍,无法做到按需加载,如果某些不想使用的类实例化很耗时,就会造成资源的浪费了;
  • 第一个点引发的问题:获取某个实现类方式不灵活,不能通过参数控制要加载什么类,每次都只能迭代获取。而在一些框架的运行时通过参数控制加载具体的类的需求是很有必要的;
  • 最后一点,ServiceLoader类的实例用于多个并发线程是不安全的。比如LazyIterator::nextService中的providers.put(cn, p);方法不是线程安全的。

基于这些缺点,目前很多中间件或者框架都会选择自行实现SPI机制,这里我们的RPC框架中也来实现一个自己的SPI,主要思路借鉴Dubbo框架。

自定义SPI实现

SPI的主要实现思路其实就是通知设置某种规则,将需要扩展的类配置到指定目录下,通过程序读取到指定的配置后,将类进行实例化,供框架使用。
为了实现SPI使用的灵活性,我们将SPI配置文件中的内容调整为key-value的格式,key为扩展的具体功能名称,value为对应类的全限定名,这样在使用的时候我们可以通过应用的配置文件去指定要创建的组件名称,和SPI机制打通,增加使用的灵活性。
在这里插入图片描述

具体SPI的加载代码如下,比较简单,不过多阐述:

public class ExtensionLoader {

    public static String EXTENSION_LOADER_DIR_PREFIX = "META-INF/irpc/";

    /**
     * key:interface name  value:{key:configName value:ImplClass}
     */
    public static Map<String, LinkedHashMap<String, Class>> EXTENSION_LOADER_CLASS_CACHE = new ConcurrentHashMap<>();

    public void loadExtension(Class clazz) throws IOException, ClassNotFoundException {
        if (clazz == null) {
            throw new IllegalArgumentException("class can not null");
        }

        String spiFilePath = EXTENSION_LOADER_DIR_PREFIX + clazz.getName();
        ClassLoader classLoader = this.getClass().getClassLoader();
        Enumeration<URL> enumeration = classLoader.getResources(spiFilePath);
        while (enumeration.hasMoreElements()) {
            URL url = enumeration.nextElement();
            InputStreamReader inputStreamReader = null;
            inputStreamReader = new InputStreamReader(url.openStream());
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String line;
            LinkedHashMap<String, Class> classMap = new LinkedHashMap<>();
            while ((line = bufferedReader.readLine()) != null) {
                //如果配置中加入了#开头,则表示忽略该类,无需加载
                if (line.startsWith("#")){
                    continue;
                }
                String[] lineArr = line.split("=");
                String implClassName = lineArr[0];
                String interfaceName = lineArr[1];
                //保存的同时初始化类
                classMap.put(implClassName,Class.forName(interfaceName));
            }

            //放入缓存中
            if (EXTENSION_LOADER_CLASS_CACHE.containsKey(clazz.getName())){
                EXTENSION_LOADER_CLASS_CACHE.get(clazz.getName()).putAll(classMap);
            }else {
                EXTENSION_LOADER_CLASS_CACHE.put(clazz.getName(),classMap);
            }
        }
    }

}

RPC框架接入

我们的RPC框架目前可扩展或指定的功能有如下:

  • 序列化方式
  • 路由策略
  • 过滤器
  • 注册中心
  • 动态代理实现(我们目前使用的默认JDK动态代理,还有其他的代理方式,如CGLIB等)

Client端调整

我们将可扩展的点都通过SPI的方式去配置,方便用户去集成我们的框架,Server端的同理,这里就不过多展示了,大家去看源码即可。

private void initConfig() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
    //初始化路由策略
    EXTENSION_LOADER.loadExtension(IRouter.class);
    String routeStrategy = CLIENT_CONFIG.getRouteStrategy();
    LinkedHashMap<String, Class> iRouterMap = EXTENSION_LOADER_CLASS_CACHE.get(IRouter.class.getName());
    Class iRouterClass = iRouterMap.get(routeStrategy);
    if (iRouterClass == null) {
        throw new RuntimeException("no match routerStrategy for " + routeStrategy);
    }
    IROUTER = (IRouter) iRouterClass.newInstance();
    //初始化序列化方式
    EXTENSION_LOADER.loadExtension(SerializeFactory.class);
    String serializeType = CLIENT_CONFIG.getClientSerialize();
    LinkedHashMap<String, Class> serializeTypeMap = EXTENSION_LOADER_CLASS_CACHE.get(SerializeFactory.class.getName());
    Class serializeClass = serializeTypeMap.get(serializeType);
    if (serializeClass == null) {
        throw new RuntimeException("no match serialize type for " + serializeType);
    }
    CLIENT_SERIALIZE_FACTORY = (SerializeFactory) serializeClass.newInstance();
    //初始化过滤链
    EXTENSION_LOADER.loadExtension(IClientFilter.class);
    ClientFilterChain clientFilterChain = new ClientFilterChain();
    LinkedHashMap<String, Class> filterMap = EXTENSION_LOADER_CLASS_CACHE.get(IClientFilter.class.getName());
    for (String implClassName : filterMap.keySet()) {
        Class filterClass = filterMap.get(implClassName);
        if (filterClass == null) {
            throw new NullPointerException("no match client filter for " + implClassName);
        }
        clientFilterChain.addServerFilter((IClientFilter) filterClass.newInstance());
    }
    CLIENT_FILTER_CHAIN = clientFilterChain;
}

总结

本版本我们对SPI机制进行了详解,并且自己实现了SPI机制,增加了原Jdk原生的SPI机制的不足,并集成在了我们的RPC框架中,后续如果想对框架中的功能进行扩展的话,通过SPI机制无需修改源代码即可完成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/144195.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

发表计算机SCI论文需要注意什么? - 易智编译EaseEditing

一篇SCI&#xff0c;除了能让审稿人浅显易懂的了解你的表达之外&#xff0c;我们还需要在内容上做好&#xff1a; 1、SCI论文标题创新、简洁 创新是因为写科技文章的目的在于报道新的科技进展&#xff0c;缺乏创新因素就会失去发表的意义。 但运用创新要建立在已有的科研成果…

QT部件透明阴影效果与不规则窗体

透明效果原始效果设置整个窗体透明&#xff0c;调用setWindowOpacity( )方法&#xff0c;传入一个0~1之间的值来表示透明度&#xff1b;1表示不透明&#xff0c;0表示完全透明setWindowOpacity(0.5);//0~1之间设置窗体透明&#xff0c;部件不透明setWindowFlags(Qt::FramelessW…

MATLAB | 赠书 | 如何从热图中提取数据

gzh上这篇文章正在抽奖赠书&#xff1a;截止日期2023年1月9日12&#xff1a;00&#xff08;周一&#xff09; MATLAB | 文末赠书 | 如何从热图中提取数据 赠送3本由北京大小出版社提供的《SPSS统计分析大全》 这期做了个可能有用的小工具&#xff0c;一般论文中热图很少给出…

十.指针进阶(对指针的深度理解)

目录 一. 字符指针 1.字符指针的定义 2.字符指针的用法 3.字符指针练习 二. 数组指针 1.指针数组的定义 2.指针数组的用法 三. 指针数组 1.数组指针的定义 2.数组名和&数组名的区别 3.数组指针的用法 4.练习 四. 数组传参和指针传参 1.一维数组传参 2.二维数…

十、MyBatisX插件

文章目录十、MyBatisX插件1 安装MyBatisX插件2 MybatisX代码速成3 在mapper接口中实现自定义功能【尚硅谷】MyBatisPlus教程-讲师&#xff1a;杨博超 失败&#xff0c;是正因你在距成功一步之遥的时候停住了脚步。 十、MyBatisX插件 MyBatis-Plus为我们提供了强大的mapper和ser…

Jdbc配置文件连接mysql8.0——通过拼接字符串进行批量增删改操作

目录 一、基类BaseDao 二、对dog表的批量增删改操作 (一)Dog类 (二)DogDao接口 (三)DogDaoImpl实现类 1.批量新增 2.批量删除 3.批量修改 (四)Test测试 1.新增 2.删除 3.修改 三、对master表进行批量增删改 (一)Master类 (二)MasterDao接口 (三)MasterDaoImpl实…

RK3588平台开发系列讲解(内核调试篇)oops分析

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、oops简介二、oops分析2.1、实验代码2.2、oops信息2.3、oops分析沉淀、分享、成长,让自己和他人都能有所收获!😄 📢当系统内核发生kernel panic的时候,系统会打印出oops信息,本篇主要介绍如何根据oops定位问…

2022/1/6总结

今天学习了KMP算法。 KMP算法 这是一个字符串查找的算法&#xff0c;我们之前学习的字符串查找都是暴力穷举&#xff0c;然而这个效率太低&#xff0c;于是有三位大佬发明了线性的KMP算法。 算法说难不难&#xff0c;说简单也不简单。 算法的核心思想是找到最长的相等的前…

Struts2框架之Action配置

Struts2框架之Action配置Action配置1、访问Action的三种方式1.1、method属性访问1.2、Action动态方法调用1.3、通配符调用2、配置默认的ActionAction配置 Action控制器在Struts2框架中至关重要&#xff0c;主要作用如下&#xff1a; 封装工作单元数据转移的场所返回结果字符串…

(黑马C++)L07 多态

一、多态的基本概念 多态是面向对象程序设计语言中除数据抽象和继承之外的第三个基本特征。 多态&#xff1a;父类的引用或者指针指向子类对象 C支持编译时多态&#xff08;静态多态&#xff09;和运行时多态&#xff08;动态多态&#xff09;&#xff0c;运算符重载和函数重…

纷享销客CRM让科顺营销人更容易呼唤到“炮火”

希望让听得见“炮声”的人&#xff0c;更容易呼唤到“炮火”。对于在一线做营销的人而言&#xff0c;他们就是听到“炮声”的人。让一线的人员听得到“炮声”也就是销售线索、商机&#xff0c;能呼唤到“炮火”也就是呼唤到他们需要的资源。这恐怕是所有营销人都希望达到的境界…

ubuntu Ad-Hoc组网通信

目录 WIFI通信的多种组网方式 1、AP模式 2、Ad-hoc模式 ubuntu18配置ad-hoc模式 WIFI通信的多种组网方式 1、AP模式 最常用的模式&#xff0c;需要一个节点&#xff08;一般是路由器&#xff09;作为AP&#xff0c;其他节点连接到这个AP产生的wifi网络。通信拓扑是星形&a…

11_6、Java集合之Map接口的使用

一、引入Map与Collection并列存在。用于保存具有映射关系的数据:key-value &#xff08;双列集合框架&#xff09;&#xff0c;Map 中的 key 和 value 都可以是任何引用类型的数据 。Map 中的 key 用Set来存放&#xff0c;不允许重复&#xff0c;即同一个 Map 对象所对应 的类&…

在rhel6系统部署iscsi远程存储

文章目录一 需求二 环境准备三 服务端配置3.1 添加硬盘3.2 安装软件3.3 编写配置文件3.4 启动服务3.5 检查配置信息四 客户端配置4.1 安装软件包4.2 启动服务4.3 发现目标4.4 登陆目标4.5 实现开机自动挂载五 对部署进行测试一 需求 1&#xff09;首先在服务端添加一块10G的硬…

实验二十二 配置访问控制列表AGL

实验二十二 配置访问控制列表AGL一、 ACL基础概念 1、访问控制列表根据源地址、目标地址、源端口或目标端口等协议信息对数据包进行过滤&#xff0c; 从而达到访问控制的目的 。可以在路由器、三层交换机等设备上使用 &#xff0c;目前部分新二层交换 机也支持ACL。 2、ACL由编…

十、k8s DashBoard

文章目录1 部署Dashboard2 使用DashBoard之前在kubernetes中完成的所有操作都是通过命令行工具kubectl完成的。其实&#xff0c;为了提供更丰富的用户体验&#xff0c;kubernetes还开发了一个基于web的用户界面&#xff08;Dashboard&#xff09;。用户可以使用Dashboard部署容…

超级浏览器的技术原理,超级浏览器的浏览器指纹是什么?

浏览器指纹是超级浏览器的识别信息&#xff0c;网站可以通过这些信息来识别用户&#xff0c;判断用户的唯一性。常见的浏览器指纹有IP地址、浏览器所在地区、时区&#xff1b;用户代理&#xff08;User Agent&#xff09;相关的操作系统及版本、CPU 类型、浏览器及版本、浏览器…

吴恩达《机器学习》——PCA降维

PCA降维1. 主成分分析1.1 数据降维动机1.2 PCA降维目标问题分析2. PCA数学原理分析2.1 求协方差矩阵的碎碎念2.2 PCA实现方法3. Python实现3.1 进行人脸数据压缩数据集、源文件可以在Github项目中获得 链接: https://github.com/Raymond-Yang-2001/AndrewNg-Machine-Learing-Ho…

一些实用的办公工具分享给你

ABBYY FineReader 这是一个可以转换PDF格式的图片文字识别软件&#xff0c;下载之后可以免费试用七天&#xff0c;或者选择去它的网站上传PDF进行识别转换&#xff0c;一天最多可以转换10次&#xff0c;且一次只能转换3个页面。 【操作方法】 打开软件&#xff0c;点击“图像…

(day3)自学Java——面向对象

非原创&#xff0c;为方便自己后期复习 目录 1.类和对象 2.封装 3.就近原则和this关键字 4.构造方法 5.标准的javabean类 6.三种情况的对象内存图 7.基本数据类型和引用数据类型 8.this的内存原理 9.面向对象综合训练 (1)文字版格斗游戏 (2)两个对象数组练习 (3)对…