该文章内容发布已经超过一年,请注意检查文章中内容是否过时。

Dubbo 2.7.x repackage 后的兼容实现方案

本文简单描述了2.7.x repackage后对老版本的兼容性实现方案。

Dubbo至加入Apache孵化器以来,一个很强的诉求就是需要rename groupId和package name,这两项工作在项目毕业前需要完成。其中rename package相对来说复杂一些,除了要修改所有类的包名为org.apache.dubbo外,更多的是需要考虑如何老版本的兼容性。

常见的兼容性包括但不限于以下几种情况:

  • 用户API
    • 编程API
    • Spring注解
  • 扩展SPI
    • 扩展Filter

2.7.x里就是通过增加了一个新的模块dubbo-compatible来解决以上兼容性问题。

编程使用API

编程使用API是最直接最原始的使用方式,其他方式诸如Spring schema、注解等方式都是基于原始API的;因此非常有必要对API编程形式进行兼容。

所有编程相关API的兼容代码均在com.alibaba.dubbo.config包下,下面我们看看几个常见API的兼容实现。

ApplicationConfig

package com.alibaba.dubbo.config;

@Deprecated
public class ApplicationConfig extends org.apache.dubbo.config.ApplicationConfig {

    public ApplicationConfig() {
        super();
    }

    public ApplicationConfig(String name) {
        super(name);
    }
}

ProtocolConfig

package com.alibaba.dubbo.config;

@Deprecated
public class ProtocolConfig extends org.apache.dubbo.config.ProtocolConfig {

    public ProtocolConfig() {
    }

    public ProtocolConfig(String name) {
        super(name);
    }

    public ProtocolConfig(String name, int port) {
        super(name, port);
    }
}

可以看到:

  1. 兼容类是直接通过继续repacakge后的类,达到最大程度的代码复用;
  2. 构造函数也需要保持兼容;

整个兼容包中,除了上述API以外,包括一些常用的类比如ConstantsURL以及绝大部分的兼容类都是通过简单的继承,让用户基于老的API实现的类能正确运行。

Spring注解

Spring注解诸如@EnableDubbo@Service以及@Reference,由于不能使用继承,故这些注解类是通过代码拷贝来实现的;用于处理这些注解的Spring BeanPostProcessor以及Parser等相关的类,也是通过拷贝来实现;

这类兼容代码分别位于兼容包的以下几个package中:

  • com.alibaba.dubbo.config.annotation
  • com.alibaba.dubbo.config.spring.context.annotation
  • org.apache.dubbo.config.spring

所以这里要特别强调的是,这类代码在2.7.x里存在2份,因此有修改的同时需要同步修改。

扩展SPI

Dubbo的SPI扩展机制,可以通过Dubbo可扩展机制实战这篇博客详细了解。

以Filter扩展为例,简单来说就是:

  1. MyFilter需要实现Filter接口

  2. 在META-INF/dubbo下,增加META-INF/dubbo/com.alibaba.dubbo.rpc.Filter,内容为:

    myFilter=com.test.MyFilter
    

看似简单的两点,对Dubbo框架来说,需要:

  1. 正确加载配置文件META-INF/dubbo/com.alibaba.dubbo.rpc.Filter
  2. 正确加载MyFilter类并执行invoke方法

下面分别介绍Dubbo框架怎么实现以上几点。

正确加载META-INF/dubbo/com.alibaba.dubbo.rpc.Filter

Dubbo SPI机制在查找配置文件时,是根据扩展点的类名来查找的,以Filter为例,在包名变为org.apache.dubbo后,查询的目录变成:

  • META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter
  • META-INF/dubbo/org.apache.dubbo.rpc.Filter
  • META-INF/services/org.apache.dubbo.rpc.Filter

但是用户之前按老的包实现的Filter,其配置是放在类似META-INF/dubbo/com.alibaba.dubbo.rpc.Filter的,如果框架不做特殊处理,是不会加载老配置的。

因此在ExtensionLoader这个类里,做了特殊的处理:

    // synchronized in getExtensionClasses
    private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if (names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                            + ": " + Arrays.toString(names));
                }
                if (names.length == 1) cachedDefaultName = names[0];
            }
        }

        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        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;
    }

可以看到,除了加载新配置外,老配置文件也会进行扫描。

正确加载MyFilter类

com.alibaba.dubbo.rpc.Filter接口除了要继承自org.apache.dubbo.rpc.Filter以外,其唯一的方法invoke也需要做特殊处理。我们看看它的方法签名:

Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

这里参数、返回值、异常都会被实现类MyFilter用到,因此这些类也需要有兼容类;而参数、返回值不同,对于接口来说是不同的方法,因此:

  • 需要在com.alibaba.dubbo.rpc.Filter里,定义老的invoke方法,MyFilter会覆盖这个方法;
  • org.apache.dubbo.rpc.Filter里的invoke方法,需要找一个地方来实现桥接,框架调用Filter链执行到新的invoke方法时,新的参数如何转换成老参数,老返回值如何转换成新的返回值;

这里就用到了JDK8的新特性:接口default方法。

@Deprecated
public interface Filter extends org.apache.dubbo.rpc.Filter {

    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

    default org.apache.dubbo.rpc.Result invoke(org.apache.dubbo.rpc.Invoker<?> invoker,
                                               org.apache.dubbo.rpc.Invocation invocation)
            throws org.apache.dubbo.rpc.RpcException {
        Result.CompatibleResult result = (Result.CompatibleResult) invoke(new Invoker.CompatibleInvoker<>(invoker),
                new Invocation.CompatibleInvocation(invocation));
        return result.getDelegate();
    }
}

可以看到,default方法里,对参数进行了包装,然后调用老的invoke方法,并将返回值进行解包后返回给Dubbo框架。这里Result.CompatibleResult、Invocation.CompatibleInvocation以及Invoker.CompatibleInvoker都用到了代理模式。

感兴趣的同学可以详细看一下以下几个类:

  • com.alibaba.dubbo.rpc.Invocation
  • com.alibaba.dubbo.rpc.Invoker
  • com.alibaba.dubbo.rpc.Result

后续todo list

目前兼容包仅仅是对常见的API及SPI做了支持,列表如下:

  • com.alibaba.dubbo.rpc.Filter / Invocation / Invoker / Result / RpcContext / RpcException
  • com.alibaba.dubbo.config.*Config
  • com.alibaba.dubbo.config.annotation.Reference / Service
  • com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo
  • com.alibaba.dubbo.common.Constants / URL
  • com.alibaba.dubbo.common.extension.ExtensionFactory
  • com.alibaba.dubbo.common.serialize.Serialization / ObjectInput / ObjectOutput
  • com.alibaba.dubbo.cache.CacheFactory / Cache
  • com.alibaba.dubbo.rpc.service.EchoService / GenericService

大家如果在试用的过程中发现有任何问题请及时提出;同时如果对其他扩展点有兼容需求,也请大家提出来,也非常欢迎大家自己解决并贡献出来。

最后修改 February 22, 2023: Merge refactor website (#2293) (4517e8c1c9)