为了账号安全,请及时绑定邮箱和手机立即绑定

Dubbo SPI

标签:
Java

前言

大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo SPI。在前面上个章节中我们讨论了 Dubbo 服务在线测试,了解了服务测试的基本使用和其实现的原理:其核心原理是通过元数据和使用 GenericService API 在不依赖接口 jar 包情况下发起远程调用。那本章节我们主要讨论在 Dubbo 中SPI拓展机制,那什么是SPI?以及其在我们的项目中有什么作用呢?那么我们在本章节中进行讨论。下面就让我们快速开始吧!

1. Dubbo SPI 简介

什么是 Dubbo SPI 呢?其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。下面来自官方的介绍:Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。

  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

  3. 增加了对扩展点 IoCAOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

我们可以简单总结:Dubbo 中 SPI 按需加载节省资源、修复了 Java SPI 因类加载类失败异常被忽略问题、增加对 IoCAOP 的支持。

2. 使用方式

在 Dubbo 中有三个路径来存放这些拓展配置:META-INF/dubboMETA-INF/dubbo/internalMETA-INF/services/第二个目录是用来存放 Dubbo 内部的 SPI 拓展使用,第一个和第三个目录是我们可以使用的目录。拓展文件内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔,文件名为类全限定名。例如:

|- resources

​ |- META-INF

​ |- dubbo

​ | - org.apache.dubbo.rpc.Filter

​ | - custom=com.muke.dubbocourse.spi.custom.CustomFilter

Tips:META-INF/services/是 JDK 提供的 SPI 路径。

3. 使用场景

Dubbo 的拓展点是 Dubbo 成为最热门的 RPC 框架原因之一,它把灵活性、可拓展性发挥到了极致。在我们定制 Dubbo 框架的时候非常有用,我们执行简单的拓展和配置即可实现强大的功能。下面我们列举日常工作中常使用到的场景:

  1. 日志打印:在服务方法调用进入打印入参日志,方法调用完成返回前打印出参日志。

  2. 性能监控:在方法调用进入开始计时,方法调用完成返回前记录整个调用耗费时间。

  3. 链路追踪:在 Dubbo RPC 调用链路中传递每个系统的调用trace id,通过整合其它的链路追踪系统进行链路监控。

4. 示例演示

下面我们同样使用一个获取图书列表实例进行演示,同时我们自定义一个Filter在调用服务前后为我们输出日志。项目的结构如下:

idea

上面的结构中我们自定义了CustomFilter代码如下:

/**
 * @author <a href="http://youngitman.tech">青年IT男</a>
 * @version v1.0.0
 * @className CustomFilter
 * @description 自定义过滤器
 * @JunitTest: {@link  }
 * @date 2020-12-06 14:28
 **/
public class CustomFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        System.out.println("自定义过滤器执行前");

        Result result = invoker.invoke(invocation);

        System.out.println("自定义过滤器执行后");

        return result;
    }
}

我们实现Filter并且在调用Invoker前后打印日志输出。下面我们看看服务提供端dubbo-provider-xml.xml配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <dubbo:protocol port="20880" />

    <!--指定 filetr key = custom -->
    <dubbo:provider filter="custom"/>

    <dubbo:application name="demo-provider" metadata-type="remote"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <bean id="bookFacade" class="com.muke.dubbocourse.spi.provider.BookFacadeImpl"/>

    <!--暴露服务为Dubbo服务-->
    <dubbo:service interface="com.muke.dubbocourse.common.api.BookFacade" ref="bookFacade" />

</beans>

上面的配置中我们配置<dubbo:provider filter="custom"/>指定使用custom自定义过滤器。接下来我们在resources->META-INF.dubbo目录下新建org.apache.dubbo.rpc.Filter文件配置内容如下:

custom=com.muke.dubbocourse.spi.custom.CustomFilter

其中custom为我们的拓展key与我们在 XML 中配置保持一致。

5. 原理分析

Dubbo 中的 SPI 拓展加载使用 ExtensionLoader下面我们简单的通过源码来分析下。首先入口为静态函数org.apache.dubbo.common.extension.ExtensionLoader#ExtensionLoader代码如下:

    private ExtensionLoader(Class<?> type) {
        //加载的拓展类类型
        this.type = type;
        //容器工厂,如果不是加载ExtensionFactory对象先执行ExtensionFactory加载再执行 getAdaptiveExtension
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

上面的方法很简单就是获得ExtensionLoader对象,值得注意的是这里是一个层层递归的调用直到加载类型为ExtensionFactory时终止。接下来我们看看getAdaptiveExtension代码:

    public T getAdaptiveExtension() {
        //缓存获取
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
           //...
            //加锁判断
            synchronized (cachedAdaptiveInstance) {
                //再次获取 双重锁检测
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        //创建拓展实例
                        instance = createAdaptiveExtension();
                        //进行缓存
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                     //...
                    }
                }
            }
        }
        return (T) instance;
    }

我们解析看看createAdaptiveExtension方法是怎样创建实例:

    private T createAdaptiveExtension() {
        try {
            //首先创建拓展实例,然后注入依赖
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

getAdaptiveExtensionClass方法代码如下:

    private Class<?> getAdaptiveExtensionClass() {
        //获取拓展类
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        //动态生成Class
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

我们主要分析getExtensionClasses核心代码如下:

 /***
     *
     * 获取所有的拓展Class
     *
     * @author liyong
     * @date 20:18 2020-02-27
     * @param
     * @exception
     * @return java.util.Map<java.lang.String,java.lang.Class<?>>
     **/
    private Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    //开始从资源路径加载Class
                    classes = loadExtensionClasses();
                   //设置缓存
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

loadExtensionClasses代码如下:

    /**
     *
     * CLASS_PATH=org.apache.dubbo.common.extension.ExtensionFactory
     *
     * 1.META-INF/dubbo/internal/${CLASS_PATH} Dubbo内部使用路径
     * 2.META-INF/dubbo/${CLASS_PATH}  用户自定义扩展路径
     * 3.META-INF/services/{CLASS_PATH} JdkSPI路径
     *
     * synchronized in getExtensionClasses
     * */
    private Map<String, Class<?>> loadExtensionClasses() {
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();
        // internal extension load from ExtensionLoader's ClassLoader first
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
        //兼容处理 由于dubbo捐献给apache
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }

接下来我们看到真正进行资源加载的方法loadDirectory

 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();
            
            // try to load from ExtensionLoader's ClassLoader first
            if (extensionLoaderClassLoaderFirst) {
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                //这里首先使用ExtensionLoader的类加载器,有可能是用户自定义加载
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }
            
            if(urls == null || !urls.hasMoreElements()) {
                if (classLoader != null) {//使用AppClassLoader加载
                    urls = classLoader.getResources(fileName);
                } else {
                    urls = ClassLoader.getSystemResources(fileName);
                }
            }

            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();
                    //加载资源
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

我们看看资源内容的加载逻辑方法loadResource核心代码如下:

    //加载文件值转换为Class到Map
    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;
                //读取一行数据
                while ((line = reader.readLine()) != null) {
                    final int ci = line.indexOf('#');//去掉注释
                    if (ci >= 0) {
                        line = line.substring(0, ci);
                    }
                    line = line.trim();
                    if (line.length() > 0) {
                        try {
                            String name = null;
                            int i = line.indexOf('=');
                            if (i > 0) {
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            }
                            if (line.length() > 0) {
                                //加载Class
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                            }
                        } catch (Throwable t) {
                           //...
                        }
                    }
                }
            }
        } catch (Throwable t) {
           //...
        }
    }

上面的方法通过循环加载每一行数据,同时解析出=后面的路径进行Class的装载。由此循环加载自定资源路径下面的所有通过配置文件配置的类。

6. 小结

在本小节中我们主要学习了 Dubbo 中 SPI,首先我们知道 Dubbo SPI 其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来,同时解决了 Java 中 SPI 的一些缺陷。我们也通过简单的使用案例来介绍我们日常工作中怎样去拓展,以及从源码的角度去解析 SPI 的加载原理其核心入口类为ExtensionLoader`。

本节课程的重点如下:

  • 理解 Dubbo 中 SPI
  • 了解 Dubbo SPI 与 Java SPI 区别
  • 了解 Dubbo 怎样使用 SPI 进行拓展
  • 了解 SPI 实现原理

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消