Dubbo的spi代理对象生成原理

By ref-nobody 创建时间 2025年4月13日 | 本文最后更新于 2025年4月26日 #Adaptive, #dubbo, #ExtensionLoader, #spi

前言

通过调试可以观察到dubbo中spi拓展生成的对象都有$Adaptive的后缀,例如protocol的spi实例化对象为:

下面来看看这个代理对象是如何生成的:
首先我们获取spi的代理对象可以通过ExtensionLoader中的两个方法getExtensiongetAdaptiveExtension来获取,这两者的区别是:
getExtension 方法用于获取指定名称的扩展点实现类实例;
getAdaptiveExtension 方法用于获取一个自适应的扩展点实例。这个实例会根据运行时的参数动态地选择具体的扩展点实现类;
1. 在代码中,使用getExtensionLoader(type)首先会获取到对应类型的spi接口的ExtensionLoader实例,如果不存在则创建,并进行缓存存放到ExtensionLoader类的extensionLoadersMap成员变量中。
创建对应类型的ExtensionLoader很简单,使用map的putIfAbsent方法,将new出来的ExtensionLoader放入缓存map中:extensionLoadersMap.putIfAbsent(type, new ExtensionLoader(type, this, scopeModel));
2. 然后调用创建出来的ExtensionLoadergetAdaptiveExtension方法获取自适应的实例。
创建的流程是首先动态生成自适应的spi类的java代码,即图中的code变量;然后对动态生成的代码进行编译Compile生成Class对象。有了Class对象之后,即可通过反射创建实例对象。

以spi拓展Protocol接口举例来说,上面的code中的代码为:

生成的拓展类源码里面会根据接口上面是否有@Adaptive注解来动态的生成方法,如果是接口的方法上面有@Adaptive注解修饰,例如refer方法,则生成如下方法内容:

如果是没有@Adaptive注解修饰的接口,则方法体内会直接抛出异常:

可以看到虽然是调用的getExtensionLoader(type).getAdaptiveExtension方法,但在生成的自适应方法内部实际上还是调用的getExtensionLoader(type).getExtension(extName),只是不需要我们指定extName,而是会通过url中的参数信息动态获取。
代码调用流程:

而对于dubbo中的spi的加载,是在程序首次访问spi拓展点的接口的时候,此时会创建指定的spi接口对应的ExtensionLoader类,并进行缓存放入extensionLoadersMap中,然后使用该类型的ExtensionLoader将对应类型的spi拓展点的实现加载进来,并且进行缓存Holder<Map<String, Class<?>>> cachedClasses;但是只实例化指定的名称对应的spi实现类,而不是所有该类型下的spi实现类。

总结出来就是首先对于每个spi接口,都会有一个对应的ExtensionLoader;对于每个spi接口的实现,在首次读取之后也会将其进行缓存,同时只实例化指定名称的spi接口实现。

动态代理生成Adaptive流程

再来详细的看看dubbo是如何生成的Adaptive类的流程。以Protocol接口为例,查看接口源码可以看到:

@SPI(value = "dubbo", scope = ExtensionScope.FRAMEWORK)
public interface Protocol {
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}

在接口上面使用了@SPI注解,并且里面的方法上面使用了@Adaptive注解来标注这个方法是自适应方法;首先看入口方法:

getAdaptiveExtensionClass方法中,首先会从spi接口的文件中加载该类型的所有的接口实现(Protocol接口的文件则为META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol)并将其放入缓存cachedClasses中。缓存的key为文件中我们给每个实现类起的名称,value则为实现类加载后对应的Class对象。除此之外,在加载的时候同时也会解析出@SPI注解中的value值,作为SPI接口的默认实现,这个值在后面动态生成代码的时候会用到。
当所有的实现类都加载完之后,则会开始动态的构建自适应类的源代码:

首先我们要知道生成的自适应类是实现了SPI接口的子类,在生成源代码的时候,源代码中的包导入、类声明、非自适应方法等部分则是通用的固定格式生成的,重点看每个@Adaptive注解修饰的自适应方法的生成方式:

因为生成的自适应方法是自动帮我们实现了SPI接口,因此需要实现接口中所有的方法。如果方法不是自适应的,则在方法体中直接抛出异常。如果方法是自适应方法,则会解析@Adaptive注解中配置的value值,如果注解没有配置,则会根据类名自动生成一个默认的值。

String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");

例如Protocol接口,则生成的值为protocolProxyFactory接口则生成的值为:proxy.factory
查看之前生成过的Protocol的自适应子类源码,自适应方法的方法体中对于extName的赋值则是动态生成的。

在dubbo的源码中,则对应于generateExtNameAssignment方法:

图中的value就是@Adaptive中配置的value值,由于如果没有配置的话,dubbo也会根据类名自动生成一个,因此方法中的入参value.length>0。代码中的defaultExtName就是前面在加载所有的SPI实现类的时候,解析到的@SPI注解上面的value值。

Protocol接口上面的@SPI注解中配置的为”dubbo”,因此动态生成的extName赋值代码为:

String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );

我们再来看另一个SPI接口RegistryFactory

@SPI(scope = APPLICATION)
public interface RegistryFactory {
    @Adaptive({PROTOCOL_KEY}) // 这个PROTOCOL_KEY就是常量protocol
    Registry getRegistry(URL url);
}

这个接口没有配置@SPI的value,并且配置了@Adaptive的value值。因此代码中的defaultExtName为null,由于配置了@Adaptive注解的value,因此generateExtNameAssignment方法入参中的value为”protocol”,因此生成的自适应类的源代码如下:

可以看到生成的自适应方法最终还是会调用getExtension(extName)方法,所以就来看看在这个方法中是如何创建对象的以及如何将对象进行包装起来的。

getExtension方法详解

getExtension是一个重载方法,参数列表为:(String name)、(String name, boolean wrap),具有两个重载,wrap参数用来选择是否将生成的实例对象进行wrapper包装。dubbo同样会将已经实例化过的spi接口的实例进行缓存,使用DCL双重检查机制检查缓存,如果缓存中不存在则创建spi接口实例对象,则进行对象的实例化:
1. 加载spi接口的实现类
从之前已经加载好的所有spi接口实现类中,找到与name匹配的实现类的Class对象;
2. 生成spi接口实现类的实例对象
通过反射将实现类Class对象进行解析获取到构造器生成实例对象,执行前置-依赖注入-后置处理逻辑;
3. 生成实例对象的包装类对象
如果需要生成实例对象的包装对象即wrapper为true,则会查找当前spi接口的包装器类。这里提到一点spi接口的包装器类是在程序启动的时候随着spi接口的加载一起加载进来的,保存在缓存cachedWrapperClasses中。除此之外,dubbo还提供了包装器行为的灵活性配置,在定义的包装器类上面可以使用@Wrapper注解来配置只包装spi接口的哪些实现类。
包装器类如何定义?通过org.apache.dubbo.common.extension.ExtensionLoader#isWrapperClass方法可以观察到:

    /**
     * test if clazz is a wrapper class
     * <p>
     * which has Constructor with given class type as its only argument
     */
    protected boolean isWrapperClass(Class<?> clazz) {
        Constructor<?>[] constructors = clazz.getConstructors();
        for (Constructor<?> constructor : constructors) {
            if (constructor.getParameterTypes().length == 1 && constructor.getParameterTypes()[0] == type) {
                return true;
            }
        }
        return false;
    }

我们定义的包装器类的构造函数中有一个参数并且参数的类型为spi接口类型,则这个类为该spi接口的包装器类。

实战自定义一个SPI接口

下面是一个代码示例,我们自定义一个SPI接口并且提供了一个实现类和一个wrapper类,代码结构如下:

@SPI()
public interface AdaptiveTest {
//    @Adaptive()
    void sayHello(String name);
}
public class AdaptiveTestImpl implements AdaptiveTest {
    @Override
    public void sayHello(String name) {
        System.out.println("AdaptiveTestImpl.sayHello");
    }
}
@Wrapper(matches = {"impl"})
public class TestAdaptiveWrapper implements AdaptiveTest {
    private AdaptiveTest wrapperObject;
    public TestAdaptiveWrapper(AdaptiveTest adaptiveTest) {
        this.wrapperObject = adaptiveTest;
    }
    @Override
    public void sayHello(String name) {
        System.out.println("TestAdaptiveWrapper.sayHello");
        wrapperObject.sayHello(name);
    }
}
// file io.itaiit.inter.AdaptiveTest
impl=io.itaiit.inter.AdaptiveTestImpl
wrap1=io.itaiit.inter.TestAdaptiveWrapper

在代码中同样也测试了@Wrapper注解的使用,运行上述的代码,我们通过AdaptiveTest adaptiveExtension = ExtensionLoader.getExtensionLoader(AdaptiveTest.class).getExtension("impl");代码调用,通过调试可以看到成功获取到经过wrapper包装过的实例对象。

我们将@Wrapper注解稍作修改为@Wrapper(matches = {"impl2222"}),由于我们没有定义名字为impl2222对应的wrapper类,因此匹配不到包装类,则获取到的仅是实现类的实例:

我们将@SPI稍作修改为@SPI("impl"),在编程中发现,如果使用了@Adaptive注解定义了自适应方法,则使用getAdaptiveExtension的时候会出现not found url parameter or url attribute in parameters of method sayHello的异常信息,这是因为自适应方法的参数中需要有URL类型的参数:

如果我们不想定义自适应方法又想实现自适应的效果,而不是在getExtension方法中指定具体的某个实现类的name,则可以传”true”: ExtensionLoader.getExtensionLoader(AdaptiveTest.class).getExtension("true")

使用Arthas查看动态代理类

在上面的讲解中都是通过调试的方式,找到Adaptive代码生成的地方打断点来查看具体的源码内容。除此之外在程序运行的时候,通过arthas工具可以直接连接上正在运行的java进程对某个class文件进行反编译。
先启用一个dubbo程序,我这里启用的是消费端的程序然后使用arthas进行连接。启用dubbo程序之后,然后运行arthas启动脚本:

输入要连接到的进程,这里是要连接到5号进程:

使用jad org.apache.dubbo.rpc.Protocol$Adaptive命令查看Protocol$Adaptive类的源码:

其他的命令可以参考arthas官网。

Leave a Reply

Your email address will not be published. Required fields are marked *

目录