概述
摘要:
本文分析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怎么做的最后补充一点所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复