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

文章介绍了如何通过SPI(ServiceProviderInterface)机制增强RPC框架的扩展性,解决了用户自定义过滤器和序列化方式需要修改源码的问题。作者首先解释了SPI的基本概念和JDK的原生实现,然后讨论了其局限性,包括按需加载和线程安全问题。接着,文章展示了如何自定义SPI实现,以提高灵活性,允许通过应用配置文件指定组件。最后,文中展示了如何在RPC框架中集成和使用这个自定义SPI,包括序列化方式、路由策略和过滤器的动态加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

系列文章:

现有的问题

在上一章节末尾我们提到了,目前我们的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机制无需修改源代码即可完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

壹氿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值