我是靠谱客的博主 缥缈月光,最近开发中收集的这篇文章主要介绍Dubbo的SPI机制实现doExportUrlsFor1Protocol 解析SPI机制动态编译又是什么最后一问:SPI怎么做的最后补充一点,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

摘要:
本文分析PROTOCOL的具体实现类是什么,SPI是如何选择默认实现类,如何通过SPI进行动态编译后,生成具体的实现类DubboProtocol。

doExportUrlsFor1Protocol 解析

上一篇我们看到了这个方法doExportUrlsFor1Protocol(),接下来我们继续详细看看。

String name = protocolConfig.getName();
if (StringUtils.isEmpty(name)) {
    name = DUBBO;
}

默认协议就是Dubbo,其底层基于Netty,后面会讲。

// 使用map将所有的参数记录起来
Map<String, String> map = new HashMap<String, String>();
map.put(SIDE_KEY, PROVIDER_SIDE);
 
//......
 
// 遍历并解析方法的配置,记录到map
//......

上面的代码就是处理方法的配置,遍历接口类的所有方法,然后读取相关的配置然后放入map中。看起来也不是关键代码。

// 如果为泛型调用,设置泛型类型
if (ProtocolUtils.isGeneric(generic)) {
    map.put(GENERIC_KEY, generic);
    map.put(METHODS_KEY, ANY_VALUE);
}
// 设置版本号和方法名称
else {
    String revision = Version.getVersion(interfaceClass, version);
    if (revision != null && revision.length() > 0) {
        map.put(REVISION_KEY, revision);
    }
 
    String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
    if (methods.length == 0) {
        logger.warn("No method found in service interface " + interfaceClass.getName());
        map.put(METHODS_KEY, ANY_VALUE);
    } else {
        map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
    }
}
 
//......

这段看起来也是在设置一些参数。

// 拼接url对象
// export service
String host = findConfigedHosts(protocolConfig, registryURLs, map);
Integer port = findConfigedPorts(protocolConfig, name, map);
URL url = new URL(name, host, port, getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), map);
 
// You can customize Configurator to append extra parameters
if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
        .hasExtension(url.getProtocol())) {
    url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
}

这段就是把前面的map最终拼凑成url。可以猜到,这个url肯定是做了很多标记性的内容,后面很多的功能执行都是依赖于这个url。
从这里我们可以学到一点东西,可以使用url这种资源定位符作为全局的参数记录,将所有参数集中在这个url中,比线程缓存更容易阅读,也容易调试。

// scope判断导出服务的形式
String scope = url.getParameter(SCOPE_KEY);
// don't export when none is configured
 
// 如果scope为SCOPE_NONE则不导出服务
if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
 
    // export to local if the config is not remote (export to remote only when config is remote)
    //  如果scope不是SCOPE_REMOTE,则导出本地服务
    if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
        exportLocal(url);
    }
    // export to remote if the config is not local (export to local only when config is local)
    //  如果scope不是SCOPE_LOCAL,则导出远程服务
    if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
        //  如果有服务注册中心地址
        if (CollectionUtils.isNotEmpty(registryURLs)) {
            for (URL registryURL : registryURLs) {
                //......
 
                Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
                DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 
                Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
                exporters.add(exporter);
            }
        }
        //  没有注册中心则是直连方式
        else {
            if (logger.isInfoEnabled()) {
                logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
            }
            Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
            DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 
            Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
            exporters.add(exporter);
        }
    }
}
this.urls.add(url);

这一段应该就是重点了,导出服务分为本地服务和远程服务,远程服务分为先注册中心、直连方式。

// Protocol层SPI
private static final Protocol PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
 
/**
* ProxyFactory的SPI
* A {@link ProxyFactory} implementation that will generate a exported service proxy,the JavassistProxyFactory is its
* default implementation
*/
private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
 
// 生成动态代理对象
Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 
// 真正导出服务
Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
exporters.add(exporter);

这段重点的代码基本都用到了Dubbo的一个特性:SPI,所以我们先跳出这个坑,去简单了解一下,SPI是什么东东。

SPI机制

Dubbo 为每个功能点提供了一个 SPI 扩展接口, Dubbo 框架在使用扩展点功能的时候是对接口进行依赖的,而一个扩展接口对应了 一系列的扩展实现类。那么如何选择使用哪一个扩展接口的实现类呢?其实这是使用适配器模式来做的。

首先我们看看什么是适配器模式,比如 Dubbo 提供的扩展接口 Protocol,其定义如下:

/**
* Protocol. (API/SPI, Singleton, ThreadSafe)
*/
@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
}

Dubbo 会使用动态编译技术为接口Protocol生成一个适配器类 Protocol$Adaptive的对象实例,在Dubbo框架中 需要使用Protocol的实例时,实际上就是使用Protocol$Adaptive的对象实例来获取具体的 SPI 实现类的。

在 Dubbo 框架中, protocol 的 定义为 :

private static final Protocol protocol =ExtensionLoader.getExtensionLoader(ProtocoI.class).getAdaptiveExtension()

,当调用 protocol.export(wrapperlnvoker)时, 实际是调用 Protocol$Adaptive 的对 象实例 的 export()方法,然 后后者根据 wrapperlnvoker 中 URL 里面的协议类型参数执行。

总结一下就是,适配器类 Protocol$Adaptive 会根据传递的协议参数的不同,力日载不同的 Protoco l 的 SPI 实现。
其实在 Dubbo 框架中 , 框架会给每个 SPI 扩展接口动态生成一个对应的适配器类,并根据参数来使用增强 SPI 以选择不同的 SPI 实现 。

动态编译又是什么

众所周知, Java 程序要想运行首先需要使用 javac 把源代码编译为 class 宇节码文件 ,然后使用 JVM 把 class 字节码文件加载到内存创建 Class 对象后,使用 Class 对象创建对象实例。正常情况下我们是把所有源文件静态编译为字节码文件, 然后由 JVM 统一加载,而动态编译则是在 JVM 进程运行时把源文件编译为字节码文件,然后使用字节码文件创建对象实例。

然后简单来说,就是dubbo内部使用了JavassistCompiler,将指定的源代码文件编译成了Class对象,有了 Class 对象后 ,我们就可以使用 newlnstance()方法创建对象实例了。

最后一问:SPI怎么做的

说了这么多,我们回来看看ExtensionLoader的源码:

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    //......
 
    // 第一次访问某个扩展接口时需要新建一个对应的ExtensionLoader 并放入缓存,后面就直接从缓存中获取 。
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

这里只是加入缓存,没什么特别。

public T getAdaptiveExtension() {
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }
 
        // 通过双重锁检查创建 cachedAdaptivelnstance 对象,接口对应的适配器对象就保存到这个对象里。
        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                   	// ......
                }
            }
        }
    }
 
    return (T) instance;
}
 
private T createAdaptiveExtension() {
    try {
        // 获取适配器对象的一个实例,然后进行依赖注入,
        // getAdaptiveExtensionClass() 实际是调用了编译组件进行动态编译类文件
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}
 
//......

/**
* 使用编译组件进行类文件的动态编译
*/
private Class<?> createAdaptiveExtensionClass() {
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    ClassLoader classLoader = findClassLoader();
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

上面的代码就是想办法从jar包中读取指定的类文件,然后使用动态编译的方式将类文件加载成类对象,接着使用newInstance()创建成对象,后面就可以直接使用该对象了。

具体的SPI机制还是有些复杂的,后面我会写1篇扩展阅读来详细讲解这块,目前在这里我们就简单理解为SPI会根据某个配置文件读取指定的类路径,然后根据类路径加载到类文件,接着使用动态编译将类文件使用类加载器加载到JVM内存,并使用类对象newInstance()创建具体的对象,这样就OK了。

最后补充一点

Protocol的默认实现是Dubbo,这点在SPI的注解就可以看到:

@SPI("dubbo")
public interface Protocol {
}

如何看Dubbo的Protocol实现类呢,直接搜索 META-INFdubbointernalorg.apache.dubbo.rpc.Protocol这些文件的配置,可以看到其中一个:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

所以实现类就是DubboProtocol。

刚开始阅读代码还是不用太纠结,先一鼓作气继续读完主流程。

最后

以上就是缥缈月光为你收集整理的Dubbo的SPI机制实现doExportUrlsFor1Protocol 解析SPI机制动态编译又是什么最后一问:SPI怎么做的最后补充一点的全部内容,希望文章能够帮你解决Dubbo的SPI机制实现doExportUrlsFor1Protocol 解析SPI机制动态编译又是什么最后一问:SPI怎么做的最后补充一点所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(46)

评论列表共有 0 条评论

立即
投稿
返回
顶部