-
源码版本
-
需求
-
原理
-
源码分析
-
总结
源码版本
2.7.9-SNAPSHOT
需求
假设要你实现这样一种需求你能实现吗?有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。简单理解就是 有一个接口
public interface Protocol {
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
}
这个方法 要通过 他的参数 Invoker<?> originInvoker
,去动态的指定他的实现类 这里就有一个类似死锁的概念: 接口方法要被调用就需要加载接口的实现类(扩展),而接口的实现类初始化又需要接口的方法被调用才加载(扩展)。想想要你,你能实现这个功能嘛?
解释:为什么不加载后再获取代理对象呢?本质也是为了性能。可见开源框架对性能的追求
原理
dubbo的自适应拓展机制就解决了上面的问题。自适应扩展机制的实现特别复杂,需要大家耐心看完,看完后你才会发现源码之美。
首先说一些核心实现思路:这里首先会通过反射获取接口的方法,然后通过接口的方法拿到类名,如果此时实现类在类似Spring容器中会比较简单,直接通过类名拿出来即可,可惜实现的需求是在使用的时候才加载扩展,所以需要拿到了类名后通过自己实现去拼接整个java类的源代码,然后通过编译器动态编译出来。
源码分析
首先源码分析的入口是方法getAdaptiveExtension()
这里我们就从Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
这行代码开始看吧
首先我们从核心方法分析吧
-
getAdaptiveExtension
public T getAdaptiveExtension() {
// 从缓存中获取自适应拓展
Object instance = cachedAdaptiveInstance.get();
// 如果缓存为空
if (instance == null) {
// 如果缓存异常不为空直接返回
if (createAdaptiveInstanceError != null) {
throw new IllegalStateException("Failed to create adaptive instance: " +
createAdaptiveInstanceError.toString(),
createAdaptiveInstanceError);
}
// 双从检查的单例
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 创建自适应拓展
instance = createAdaptiveExtension();
// 设置缓存
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
}
return (T) instance;
}
上面的代码逻辑很简单,从缓存中获取扩展对象,获取不到看看缓存中是否有之前创建的异常,有就直接返回,没有就基于双从检查去创建扩展类即方法createAdaptiveExtension()
所以下面我们就重点分析 方法
-
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()
方法获取class -
反射初始化实例 -
调用 injectExtension()
方法向扩展实例注入依赖。
Dubbo 中有两种类型的自适应拓展,一种是手工编码的,一种是自动生成的。手工编码的自适应拓展中可能存在着一些依赖,而自动生成的 Adaptive 拓展则不会依赖其他类。这里调用 injectExtension 方法的目的是为手工编码的自适应拓展注入依赖
下面我们来分析方法
-
getAdaptiveExtensionClass()
private Class<?> getAdaptiveExtensionClass() {
// 通过 SPI 获取所有的拓展类
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
getAdaptiveExtensionClass()也保护三个逻辑:
-
调用 getExtensionClasses 获取所有的拓展类 -
检查缓存,若缓存不为空,则返回缓存 -
若缓存为空,则调用 createAdaptiveExtensionClass 创建自适应拓展类
这里需要注意getExtensionClasses()获取所有实现类,如果实现类中有类包含注解@Adaptive 则将该类放入cachedAdaptiveClass中,然后直接返回了,不再创建自适应扩展类
具体代码:
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
这里我们还是回归主线看方法
-
createAdaptiveExtensionClass()
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();
// 编译代码,生成 Class
return compiler.compile(code, classLoader);
}
这段代码就特别有意思。首先会构建自适应拓展代码的源代码即 class,然后找到类加载器,通过编译器编译源码获取到class
这里我们重点关注方法
-
generate()
public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
// 若所有的方法上均无 Adaptive 注解,则抛出异常
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
// 生成 package + type 所在包
// 类似 package org.apache.dubbo.rpc;
code.append(generatePackageInfo());
// 生成 import 代码:import + ExtensionLoader 全限定名
// 类似 import org.apache.dubbo.common.extension.ExtensionLoader;
code.append(generateImports());
// 生成类代码:public class + type简单名称 + $Adaptive + implements + type全限定名 + {
// 类似 public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
code.append(generateClassDeclaration());
// 获取类所有方法
Method[] methods = type.getMethods();
for (Method method : methods) {
//生成方法
code.append(generateMethod(method));
}
code.append("}");
if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
return code.toString();
}
这里我们再分析下
-
generateMethod(method)
方法
private String generateMethod(Method method) {
// 方法返回值
String methodReturnType = method.getReturnType().getCanonicalName();
// 方法名
String methodName = method.getName();
String methodContent = generateMethodContent(method);
// 方法参数
String methodArgs = generateMethodArguments(method);
// 方法异常
String methodThrows = generateMethodThrows(method);
// 占位符 填充
return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
可以看到就是简单的字符串拼接。generateMethodContent(method) 方法就是 测方法是否有注解Adaptive,没有注解直接抛出类似下面的异常其次就是 获取URL数据,为其生成判空和赋值代码。以 Protocol 的 refer 和 export 方法为例,上面的代码为它们生成如下内容(代码已格式化):
public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}

可以看看源码
-
generateMethodContent(method)
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
// 遍历参数列表,确定 URL 参数位置
int urlTypeIndex = getUrlTypeIndex(method);
// found parameter in URL type
// urlTypeIndex != -1,表示参数列表中存在 URL 参数
if (urlTypeIndex != -1) {
// Null Point check
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
code.append(generateUrlAssignmentIndirectly(method));
}
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
boolean hasInvocation = hasInvocationArgument(method);
code.append(generateInvocationArgumentNullCheck(method));
// 生成代码:
// String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); 或
// String extName = url.getMethodParameter(methodName, "loadbalance", "random"); 或
// String extName = url.getParameter("client", url.getParameter("transporter", "netty")); 或其他
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
// 生成拓展获取代码,格式如下:
// type全限定名 extension = (type全限定名)ExtensionLoader全限定名
// .getExtensionLoader(type全限定名.class).getExtension(extName);
// Tips: 格式化字符串中的 %<s 表示使用前一个转换符所描述的参数,即 type 全限定名
code.append(generateExtensionAssignment());
// return statement 如果方法返回值类型非 void,则生成 return 语句。
// 生成目标方法调用逻辑 extension.方法名(arg0, arg2, ..., argN);
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
以 Protocol 接口 debug 看看这段代码生成的代码 首先是 Method 里面的东西
然后就是方法返回值(代码已格式化)
if (arg1 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
总结
自此dubbo自适应spi源码就分析完了。整体来说核心就是通过参数获取class Name,然后就是自己通过类名,拼接一个java类出来,然后再调用类加载器和编译器动态编译出class来。以前没看过源码这种操作是敢都不敢想的,算是拓展视野吧。如果以后有面试官问你dubbo SPI 自适应拓展源码,你就可以吊打他了
后续如果需要流程图可以留言说明,后续需要就补上。虽然这次画了流程图,但我觉得并不是很详细,所以就不贴出来了。由于本人技术有限,如果上面有什么错误请及时指出。
原文始发于微信公众号(小奏技术):dubbo源码分析之SPI 自适应拓展(非dubbo spi)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30468.html