DangerWind-RPC-framework---四、SPI

SPI 即 Service Provider Interface ,可以理解为专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。参考Dubbo的SPI机制,来实现本RPC框架的SPI部分。

举个例子,client端在与server端进行通信时,需要对消息进行序列化。序列化时可以使用序列化算法有很多,包括Hessian、Kryo、ProtoStuff。系统的需求是根据消息中的序列化算法名称来调用相关序列化算法对应的类中的方法来进行序列化与反序列化,加之为了便于扩展,需要使用SPI来进行解耦。

SPI的使用方式如下:

java 复制代码
Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
                    .getExtension(codecName);

codecName是序列化算法名称,需要根据该名称加载出对应的类。

java 复制代码
    private final Class<?> type;

    private ExtensionLoader(Class<?> type) {
        this.type = type;
    }

    // 每个SPI接口都有自身的ExtensionLoader
    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.");
        }
        if (type.getAnnotation(SPI.class) == null) {
            throw new IllegalArgumentException("Extension type must be annotated by @SPI");
        }
        // firstly get from cache, if not hit, create one
        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;
    }

每个SPI接口都有自身的ExtensionLoader,调用getExtensionLoader时,首先会进行一系列的合法检查操作,之后会尝试获取该接口的ExtensionLoader,先尝试本地缓存CHM中获取,获取不到的话再创建Loader对象。

之后通过getExtension获取实例,实例也进行了本地缓存,缓存中没有的话再创建实例。

java 复制代码
    private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

    public T getExtension(String name) {
        if (StringUtil.isBlank(name)) {
            throw new IllegalArgumentException("Extension name should not be null or empty.");
        }
        // firstly get from cache, if not hit, create one
        // 缓存holder
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }
        // create a singleton if no instance exists
        // holder为空,双重检查锁创建示例
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

获取到类的Class对象后可以通过反射的方式创建此对象。

java 复制代码
   // 缓存   
   private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();

   private T createExtension(String name) {
        // load all extension classes of type T from file and get specific one by name
        // SPI接口对应的实现类,其标识名与class文件的映射,根据标识名获取class
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw new RuntimeException("No such extension of name " + name);
        }
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            try {
                // 缓存中不存在,则创建实例
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }
        return instance;
    }

关键是获取Class对象的过程 ,即getExtensionCalsses方法:

java 复制代码
    // 该SPI接口所有实现类的标识与其Class对象的缓存
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>(); 

    private static final String SERVICE_DIRECTORY = "META-INF/extensions/";  

    private Map<String, Class<?>> getExtensionClasses() {
        // get the loaded extension class from the cache
        // 根据Interface实现类的类名获取对应类的缓存
        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);
                    // 将Map集合存储在Holder中进行缓存
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    private void loadDirectory(Map<String, Class<?>> extensionClasses) {
        // 固定路径下的文件,SPI接口的类名作为文件名,在此文件中规定需要加载的实现类
        String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
        try {
            Enumeration<URL> urls;
            // 系统类加载器,它能够加载用户类路径(ClassPath)上的类和资源。对于SPI机制尤为重要,因为SPI的实现类通常是由应用程序提供并放置在应用程序的类路径下的
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            // 获取当前类加载器加载的URL资源,文件名确定一般urls是唯一的
            urls = classLoader.getResources(fileName);
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    URL resourceUrl = urls.nextElement();
                    // 使用classLoader加载资源,资源目标在resourceUrl下,加载后的class存储在extensionClasses Map集合当中
                    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);
                            // 在map中保存
                            extensionClasses.put(name, clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        log.error(e.getMessage());
                    }
                }

            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
java 复制代码
kyro=github.javaguide.serialize.kyro.KryoSerializer
protostuff=github.javaguide.serialize.protostuff.ProtostuffSerializer
hessian=github.javaguide.serialize.hessian.HessianSerializer

逐层的方法调用,实现了加载META-INF/extensions/路径下对应SPI配置文件从而加载Class对象并获取实例的过程,重要部分可参考注释。

需要注意META-INF/extensions/下的文件名需要与代码里一致,代码里指定的文件名是SPI接口类的全类名。文件里的内容也需要按照(实现类标识=实现类全类名)来编写,这样才能与代码一致,程序才可以正确解析文件,并使用类加载器加载对应的Class。最后按照<实现类标识,实现类Class对象>进行缓存。

由于(类标识,Class对象)、(Class对象,对象实例)、(类标识,对象实例)这三个缓存的存在,后续可以直接传入标识获取到对应类的实例,也优化了RPC框架的性能。

相关推荐
PersistJiao34 分钟前
Spark 分布式计算中网络传输和序列化的关系(一)
大数据·网络·spark
黑客Ash3 小时前
【D01】网络安全概论
网络·安全·web安全·php
->yjy3 小时前
计算机网络(第一章)
网络·计算机网络·php
摘星星ʕ•̫͡•ʔ4 小时前
计算机网络 第三章:数据链路层(关于争用期的超详细内容)
网络·计算机网络
.Ayang5 小时前
SSRF漏洞利用
网络·安全·web安全·网络安全·系统安全·网络攻击模型·安全架构
好想打kuo碎5 小时前
1、HCIP之RSTP协议与STP相关安全配置
网络·安全
虚拟网络工程师6 小时前
【网络系统管理】Centos7——配置主从mariadb服务器案例(下半部分)
运维·服务器·网络·数据库·mariadb
JosieBook7 小时前
【网络工程】查看自己电脑网络IP,检查网络是否连通
服务器·网络·tcp/ip
黑客Ash9 小时前
计算机中的网络安全
网络·安全·web安全