IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【自己动手实现一个简单的RPC框架】8、[v4.3]增加SPI机制 -> 正文阅读

[网络协议]【自己动手实现一个简单的RPC框架】8、[v4.3]增加SPI机制

本文代码地址:https://github.com/MSC419/msc-rpc-framework

0.实现的改进

1.增加SPI机制,方便我们为程序提供扩展功能

1.SPI机制

参考:

搞懂dubbo的SPI扩展机制 - 知乎 (zhihu.com)

从零开始实现简单 RPC 框架 2:扩展利器 SPI - 小新是也 - 博客园 (cnblogs.com)

https://github.com/Snailclimb/guide-rpc-framework

SPI:全称为 Service Provider Interface,是一种服务发现机制。在项目中一个功能接口可由多种不同技术实现,这称为程序的可扩展性,SPI机制就是为了在不修改程序框架的情况下实现程序扩展

1.1 Java SPI

1.1.1 示例

先看看Java SPI 的代码,代码目录:

image-20220530112844510

1.定义接口

public interface Car {
    void say();
}

2.编写实现类

public class Tractor implements Car {
    @Override
    public void say() {
        System.out.println("我是拖拉机");
    }
}
public class Automobile implements Car {
    @Override
    public void say() {
        System.out.println("我是汽车");
    }
}

3.META-INF/services 文件夹下创建一个文件,名称为功能接口Car 的路径com.wx.car.Car,文件内容为接口实现类的路径名

com.wx.car.impl.Automobile
com.wx.car.impl.Tractor

4.测试

public static void main(String[] s){
    System.out.println("======this is SPI======");
    ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
    Iterator<Car> cars = serviceLoader.iterator();
    while (cars.hasNext()) {
        cars.next().say();
    }
}

image-20220530114031659

从测试结果可以看出,两个实现类被成功加载,并输出了相应的内容。但我们并没有在代码中显示指定Car的类型,这就是java原生的SPI机制在发挥作用。

1.1.2 SPI作用

将接口实现类的全限定名写在META-INF/services/接口类名的配置文件中,服务加载器会读取配置文件,实例化实现类。在运行时,动态为接口替换实现类。这个特性让我们可以很容易为程序提供拓展功能,达到动态可插拔的效果。

1.1.3 源码分析

通过追踪查看ServiceLoader.load()源码,发现程序会读取META-INF/services/接口路径名文件

private static final String PREFIX = "META-INF/services/";
...
if (configs == null) {
    try {
        String fullName = PREFIX + service.getName();
        if (loader == null)
            configs = ClassLoader.getSystemResources(fullName);
        else
            //读取 META-INF/services/接口路径名 配置文件
            configs = loader.getResources(fullName);
    } catch (IOException x) {
        fail(service, "Error locating configuration files", x);
    }
}
while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
        return false;
    }
    //取出配置文件里的接口实现类路径名
    pending = parse(service, configs.nextElement());
}
nextName = pending.next();

再通过反射Class.forName()加载类对象,并用instance()方法将类实例化,从而完成了服务发现。

String cn = nextName;//配置文件里的接口实现类路径名
nextName = null;
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);//通过反射加载接口实现类对象
} catch (ClassNotFoundException x) {
    fail(service,
         "Provider " + cn + " not found");
}
...
try {
    S p = service.cast(c.newInstance());//将其实例化
    providers.put(cn, p);//将(实现类路径名,实现类实例)放入providers
    return p;
} catch (Throwable x) {
    fail(service,
         "Provider " + cn + " could not be instantiated",
         x);
}

再通过serviceLoader.iterator()providers里的实现类实例转为Iterator,供调用者使用

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        ...

Java SPI 在查找实现类的时候,需要遍历配置文件中定义的所有实现类,而这个过程会把所有实现类都实例化。一个接口如果有很多实现类,而我们只需要其中一个的时候,就会产生其他不必要的实现类。

1.2 Dubbo SPI

1.2.1 示例

image-20220530163259385

将Java项目引入mevan框架,引入Dubbo依赖,接口Car上加上@SPI,配置文件com.wx.car.Car放入META-INF/dubbo 路径下。注意新建文件的时候一级一级新建,确保是两级目录META-INF/dubbo,而不是一级目录META-INF.dubbo。Dubbo SPI 是通过键值对的方式进行配置,这样就可以按需加载指定的实现类

automobile=com.wx.car.impl.Automobile
tractor=com.wx.car.impl.Tractor

测试:

public static void main(String[] s){
    System.out.println("======dubbo SPI======");
    ExtensionLoader<Car> extensionLoader =
            ExtensionLoader.getExtensionLoader(Car.class);
    Car automobile = extensionLoader.getExtension("automobile");
    automobile.say();
    Car tractor = extensionLoader.getExtension("tractor");
    tractor.say();
}

image-20220530163815175

1.2.2 源码分析

Dubbo通过extensionLoader.getExtension("automobile")获取实例化对象,先从缓存列表中取,如果为空,则创建该实例化对象

public T getExtension(String name) {
    if (name != null && name.length() != 0) {
        if ("true".equals(name)) {
            //返回默认扩展对象
            return this.getDefaultExtension();
        } else {
            //Holder:持有目标对象
            //先通过name从缓存实例里找
            Holder<Object> holder = (Holder)this.cachedInstances.get(name);
            if (holder == null) {
                //如果没有,就新建一个空Holder并存入cachedInstances缓存实例
                this.cachedInstances.putIfAbsent(name, new Holder());
                holder = (Holder)this.cachedInstances.get(name);
            }
		   //从Holder中获取实例对象
            Object instance = holder.get();
            //如果为空,则加锁创建该实例对象
            if (instance == null) {
                synchronized(holder) {
                    instance = holder.get();
                    if (instance == null) {//双重保险
                        instance = this.createExtension(name);//创建实例化对象
                        holder.set(instance);
                    }
                }
            }

            return instance;
        }
    } else {
        throw new IllegalArgumentException("Extension name == null");
    }
}

创建实例化对象:

private T createExtension(String name) {
    // 从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表
    //详细步骤:先调用this.getExtensionClasses()从缓存类cachedClasses中取,如果cachedClasses为空,
    //再调用loadExtensionClasses(),从META-INF/dubbo/文件里加载
    Class<?> clazz = (Class)this.getExtensionClasses().get(name);
    if (clazz == null) {
        throw this.findException(name);
    } else {
        try {
            //先从EXTENSION_INSTANCES取实例
            T instance = EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                //没有,就通过反射创建实例并存入EXTENSION_INSTANCES
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = EXTENSION_INSTANCES.get(clazz);
            }
		   ...
            return instance;
        } catch (Throwable var7) {
		   ...
    }
}

1.3 RPC SPI

Dubbo SPI相比Java SPI有很多优点,如:

  • 配置文件改为键值对形式,可以通过名字获取任一实现类,而无需加载所有实现类,节约资源;
  • 增加了缓存来存储实例

所以我们设计SPI时参照Dubbo的SPI。首先我们需要@SPIExtensionLoader以及其调用的Holder

@SPI:放在接口类

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
}

Holder:跟Dubbo一致,用于持有目标对象

public class Holder<T> {

    private volatile T value;

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
}

ExtensionLoader:也是参照Dubbo的ExtensionLoader来设计的

@Slf4j
public final class ExtensionLoader<T> {
    /**
     * 扩展类存放的目录地址
     */
    private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
    /**
     * 扩展加载器缓存 {类型:加载器}
     */
    private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
    /**
     * 扩展类实例缓存 {类型:实例}
     */
    private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();

    /**
     * 扩展类的类型
     */
    private final Class<?> type;
    /**
     * 扩展实例类的Holder缓存 {name: 持有该实例的Holder}
     */
    private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    /**
     * 扩展类配置列表缓存 {type: {name, 扩展类}}
     */
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

    /**
     * 构造函数
     */
    private ExtensionLoader(Class<?> type) {
        this.type = type;
    }

    /**
     * @Description 根据类型得到扩展类加载器
     * @param type  扩展类类型
     * @Return      扩展类加载器
     */
    public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {
        //扩展类不能为空
        if (type == null) {
            throw new IllegalArgumentException("Extension type should not be null.");
        }
        //扩展类必须是接口
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type must be an interface.");
        }
        //扩展类必须有@SPI
        if (type.getAnnotation(SPI.class) == null) {
            throw new IllegalArgumentException("Extension type must be annotated by @SPI");
        }
        // 先从EXTENSION_LOADERS缓存里取
        ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        // 如果缓存里没有,就新建一个,并放在缓存里
        if (extensionLoader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
            extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        }
        return extensionLoader;
    }

    /**
     * @Description 根据名字获取扩展类实例对象
     * @param name  扩展类在配置文件中配置的名字
     * @Return      扩展类实例对象
     */
    public T getExtension(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Extension name should not be null or empty.");
        }
        // 先从缓存cachedInstances里找
        Holder<Object> holder = cachedInstances.get(name);
        // 如果没有,新建一个空Holder放进cachedInstances
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }
        // create a singleton if no instance exists
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {//双重保险
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

    /**
     * 创建对应名字的扩展类实例对象
     *
     * @param name 扩展名
     * @return 扩展类实例对象
     */
    private T createExtension(String name) {
        //  从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw new RuntimeException("No such extension of name " + name);
        }
        //先从EXTENSION_INSTANCES取实例
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            try {
                //没有,就通过反射创建实例并存入EXTENSION_INSTANCES
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            } catch (Exception e) {
                log.error(e.getMessage());
                throw new RuntimeException("Fail to create an instance of the extension class " + clazz);
            }
        }
        return instance;
    }

    /**
     * 获取当前类型{@link #type}的所有扩展类
     *
     * @return {name: clazz}
     */
    private Map<String, Class<?>> getExtensionClasses() {
        // get the loaded extension class from the cache
        Map<String, Class<?>> classes = cachedClasses.get();
        // double check
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = new HashMap<>();
                    // load all extensions from our extensions directory
                    loadDirectory(classes);
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    /**
     * 从资源文件中加载所有扩展类
     *
     * @return {name: 扩展类}
     */
    private void loadDirectory(Map<String, Class<?>> extensionClasses) {
        String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
        try {
            Enumeration<URL> urls;
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            urls = classLoader.getResources(fileName);
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    URL resourceUrl = urls.nextElement();
                    loadResource(extensionClasses, classLoader, resourceUrl);
                }
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
            String line;
            // read every line
            while ((line = reader.readLine()) != null) {
                // get index of comment
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // string after # is comment so we ignore it
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        final int ei = line.indexOf('=');
                        String name = line.substring(0, ei).trim();
                        String clazzName = line.substring(ei + 1).trim();
                        // our SPI use key-value pair so both of them must not be empty
                        if (name.length() > 0 && clazzName.length() > 0) {
                            Class<?> clazz = classLoader.loadClass(clazzName);
                            extensionClasses.put(name, clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        log.error(e.getMessage());
                    }
                }

            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

总结

学到的知识

1.认识IDEA图标:前者可以新建Java文件,后者不可以新建Java文件

image-20220530112955207

2.新建文件夹时注意,没有空心圆标志的文件夹,命名为a.b意思不是两级文件夹a/b,意思是这个文件夹叫“a.b”

image-20220530163112963

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-06-08 19:15:14  更:2022-06-08 19:15:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 15:17:00-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码