🌟 欢迎关注个人技术公众号:野区杰西
前言
在上篇文章 中,我分析了 Dubbo SPI
的源码,同时也在文章埋了一些点,专门是为了希望在后面学习 Dubbo 的时候如果需要完善的话继续去完善它的知识。今天我主要想讲的一个知识是 注解 @Adaptive 实现原理。这个注解有什么特别的呢?它这个注解其实也是扩展内容中的一部分,是 Dubbo
利用 SPI
的机制用于动态的加载扩展类,在运行时动态的时候通过一种机制来进行动态的匹配扩展类,然后通过扩展类提供服务。
@Adaptive
@Adaptive
是一种扩展机制。我们可以将这个注解作用于对象或方法上面。如果方法上有 @Adaptive
,如果是在方法上 Dubbo
会为这个接口生成一个子类,其他没标注的方法则默认抛出异常,我们可以看 Protocol
接口的描述; 如果 @Adaptive
作用于类上,Dubbo
会直接选择这个类为适配器的实现类,我们可以 AdaptiveCompiler
或 AdaptiveExtensionFactory
。
举个例子,我们来看 Dubbo
服务暴露的主要配置类 ServiceConfig
。我们可以看到它的成员变量 protocol
其实是由 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()
进行赋值的。从方法名字 getAdaptiveExtension
的字面意思上看,其实我们仅仅是知道这个是获取一个适配插件。但是里面究竟做了写什么?它是怎么获取到适配类的呢?我们重新从源码出发!我快速重温一下它调用的源码:
- 调用
getExtensionLoader
加载Protocol
的ExtensionLoader
- 调用
getAdaptiveExtension
开始加载适配器- 从 cachedAdaptiveInstance 缓存获取对应的适配器
- 如果没有,调用
createAdaptiveExtension
生成Adaptive
- 继续调用
getAdaptiveExtensionClass
生成Adaptive
- 调用
getExtensionClasses
开始加载所有插件(包括cachedAdaptiveClass
)- 如果
cachedAdaptiveClass
也没有加载到Adaptive
- 调用
createAdaptiveExtensionClass
调用编译器生成一个代理适配器类- 调用
createAdaptiveExtensionClassCode
方法声称对应的类的代码字符串 - 通过
Compiler
来进行编译生成
- 调用
- 调用
- 如果有,就直接返回
- 如果
- 调用
- 继续调用
- 如果有,返回
Adaptive
的实例对象
- 如果没有,调用
- 从 cachedAdaptiveInstance 缓存获取对应的适配器
上面的步骤其实很清晰,createAdaptiveExtensionClassCode
方法就是我想讲的核心方法。但是在讲这个方法之前,我想说说我对 Dubbo
要做这件事情的一个思考,如有错误,欢迎大家砸砖更正。
思想
这个方法主要是服务于被 @Adaptive
注解的方法。Dubbo
会扫描到,然后帮其生成一个代理方法。可能你会发出疑问:ExtensionLoader
不是已经提供了很多方法来支持扩展了吗?例如说 getExtensionClass(String name)
、getExtension(String name)
等。但是有没有发现,其实这样的灵活度还是不够高,我们只能按照 Dubbo 的这几个方法来调用扩展,我们无法有一些自己自定义的扩展方法。这样显得不够灵活!
那么我们怎么可以利用 Dubbo
的 SPI
来进一步扩展呢?其实 Dubbo
是这么干的。URL
(RFC1738――Uniform Resource Locators
) 在 Dubbo
中是非常重要的东西。因为 Dubbo
中无论是服务消费方,或者服务提供方,或者注册中心。都是通过 URL
进行定位资源的。然后 Dubbo
就是通过 URL
来作为扩展的点。例如说,在 URL 定义一个属性,可以通过 URL 的属性来选择在 Dubbo 究竟用那个实现类来处理。这听起来就很简单了,但是实践出真知!我们上一个栗子 show
一下吧。
首先我们的业务场景是,有一个用户登录的接口,它有两个对应不同实现登录的子类。那么我们分别进行实现吧。
UserService.java
@SPI("normal")
public interface UserService {
@Adaptive
public void login();
}
然后实现常规用户登录 NormalUserServiceImpl.java
public class NormalUserServiceImpl implements UserService{
@Override
public void login() {
System.out.println("正常用户登录");
}
}
然后实现 VIP
用户登录 VipUserServiceImpl.java
public class VipUserServiceImpl implements UserService{
@Override
public void login() {
System.out.println("VIP 用户登录");
}
}
因为我们需要被 Dubbo
的扩展机制检测到,所以我们在 resources
目录新建一个文件夹 META-INF/dubbo
,然后新建一个 File
,命名是以 UserService
的包路径。我这里是 com.dubbo.demo.provider.Demo
,所以我的文件命名为 com.dubbo.demo.provider.Demo.UserService
。然后填入内容
normal=com.dubbo.demo.provider.Demo.NormalUserServiceImpl
vip=com.dubbo.demo.provider.Demo.VipUserServiceImpl
ok,下面让我们写一个 Test 来检测。
public class Test {
public static void main(String[] args) {
// 创建一个 URL 对象,模拟 Dubbo 的 URL
URL url = URL.valueOf("dubbo://192.168.0.101:20880?user.service=normal");
// 获取一个UserService对象
UserService service = ExtensionLoader.getExtensionLoader(UserService.class)
.getAdaptiveExtension();
// 调用方法
service.login();
}
}
由于我们在 UserService
的 SPI
注解上有 normal
,所以我们的运行结果会调用 NormalUserServiceImpl.java
。
正常用户登录
当我们需要切换实现可以通过 URL
将 user.service
指向 vip
,那么我们就会调用 VipUserServiceImpl.java
,这个可以自己的去尝试。
或许有人说,是不是一定要 @Adaptive
实现的方法的形参都是要有 URL
?这个问题的回答是“一定要有”!那么是不是一定是 URL
对象呢?这个问题的回答是“不一定哦”!至于为什么,我们开始看 createAdaptiveExtensionClassCode
的源码解析吧!
createAdaptiveExtensionClassCode 解析
我们直接看 ExtensionLoader.java
的 createAdaptiveExtensionClassCode
方法。这段代码挺长的,但是围绕的核心就是:对带有注解的方法生成一个代理类的代码字符串。所以围绕这个核心就非常简单了。
private String createAdaptiveExtensionClassCode() {
//创建一个字符串
StringBuilder codeBuilder = new StringBuilder();
//获取被代理类的方法
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
//循环方法
for (Method m : methods) {
//查看有没有 @Adaptive 注解
if (m.isAnnotationPresent(Adaptive.class)) {
hasAdaptiveAnnotation = true;
break;
}
}
// 如果没有对应的注解修饰就不需要生成
if (!hasAdaptiveAnnotation)
throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
//添加 package、import信息
//将新生成代理的类以 “接口名称$Adaptive” 形式命名
codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");
//循环方法
for (Method method : methods) {
//获取方法的返回类型、形式参数类型、异常类型
Class<?> rt = method.getReturnType();
Class<?>[] pts = method.getParameterTypes();
Class<?>[] ets = method.getExceptionTypes();
//再次获取对应方法 @Adaptive 注解
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
//如果为空抛异常
if (adaptiveAnnotation == null) {
code.append("throw new UnsupportedOperationException(\"method ")
.append(method.toString()).append(" of interface ")
.append(type.getName()).append(" is not adaptive method!\");");
} else {
int urlTypeIndex = -1;
for (int i = 0; i < pts.length; ++i) {
if (pts[i].equals(URL.class)) {
urlTypeIndex = i;
break;
}
}
// !!!这里是从方法的形参中找到 URL
if (urlTypeIndex != -1) {
// Null Point check
String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
urlTypeIndex);
code.append(s);
s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
code.append(s);
}
// !!!这里如果找不到 URL 就看形参方法里面的对象,有没有对象有 getUrl 的方法!!!
// 注意这里已经解答了上面的疑问了!!!
else {
String attribMethod = null;
// find URL getter method
LBL_PTS:
for (int i = 0; i < pts.length; ++i) {
Method[] ms = pts[i].getMethods();
for (Method m : ms) {
String name = m.getName();
if ((name.startsWith("get") || name.length() > 3)
&& Modifier.isPublic(m.getModifiers())
&& !Modifier.isStatic(m.getModifiers())
&& m.getParameterTypes().length == 0
&& m.getReturnType() == URL.class) {
urlTypeIndex = i;
attribMethod = name;
break LBL_PTS;
}
}
}
//如果这里都没有,说明真没有了,抛异常吧
if (attribMethod == null) {
throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
+ ": not found url parameter or url attribute in parameters of method " + method.getName());
}
// 没有被 @Adaptive 修饰的方法自动生成一个“不支持调用”的异常
String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
urlTypeIndex, pts[urlTypeIndex].getName());
code.append(s);
s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
code.append(s);
s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
code.append(s);
}
//获取 @Adaptive 的 value 属性
String[] value = adaptiveAnnotation.value();
//如果没有设置,就生成一个值作为该接口在 URL 中的 key
if (value.length == 0) {
char[] charArray = type.getSimpleName().toCharArray();
StringBuilder sb = new StringBuilder(128);
for (int i = 0; i < charArray.length; i++) {
if (Character.isUpperCase(charArray[i])) {
if (i != 0) {
sb.append(".");
}
sb.append(Character.toLowerCase(charArray[i]));
} else {
sb.append(charArray[i]);
}
}
value = new String[]{sb.toString()};
}
boolean hasInvocation = false;
//这里又循环方法形式参数
for (int i = 0; i < pts.length; ++i) {
if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
// Null Point check
String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
code.append(s);
s = String.format("\nString methodName = arg%d.getMethodName();", i);
code.append(s);
hasInvocation = true;
break;
}
}
//cachedDefaultName 是在 @SPI 缓存的名字
String defaultExtName = cachedDefaultName;
String getNameCode = null;
//这个循环主要是生成代码里面的一个判断,就是如果没有指定实现哪个子类取默认的实现
//例如 userService 这里会生成 url.getParameter("user.service", "normal")
for (int i = value.length - 1; i >= 0; --i) {
if (i == value.length - 1) {
if (null != defaultExtName) {
//判断是否等于 protocal(这个是默认的协议的名字)
if (!"protocol".equals(value[i]))
//查看有没有 invocation
if (hasInvocation)
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
else
getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
else
getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
} else {
if (!"protocol".equals(value[i]))
if (hasInvocation)
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
else
getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
else
getNameCode = "url.getProtocol()";
}
} else {
if (!"protocol".equals(value[i]))
if (hasInvocation)
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
else
getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
else
getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
}
}
//赋值
code.append("\nString extName = ").append(getNameCode).append(";");
//检查变量 extName 是否为空
String s = String.format("\nif(extName == null) " +
"throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
type.getName(), Arrays.toString(value));
code.append(s);
//这里通过 ExtensionLoader.getExtension 方法获取
s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
code.append(s);
// 生成返回值
if (!rt.equals(void.class)) {
code.append("\nreturn ");
}
s = String.format("extension.%s(", method.getName());
code.append(s);
for (int i = 0; i < pts.length; i++) {
if (i != 0)
code.append(", ");
code.append("arg").append(i);
}
code.append(");");
}
codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
for (int i = 0; i < pts.length; i++) {
if (i > 0) {
codeBuilder.append(", ");
}
codeBuilder.append(pts[i].getCanonicalName());
codeBuilder.append(" ");
codeBuilder.append("arg").append(i);
}
codeBuilder.append(")");
if (ets.length > 0) {
codeBuilder.append(" throws ");
for (int i = 0; i < ets.length; i++) {
if (i > 0) {
codeBuilder.append(", ");
}
codeBuilder.append(ets[i].getCanonicalName());
}
}
codeBuilder.append(" {");
codeBuilder.append(code.toString());
codeBuilder.append("\n}");
}
codeBuilder.append("\n}");
return codeBuilder.toString();
}
因为上面代码需要严谨但是可能读起来可能会很繁杂,所以我以 UserService
为例子,贴出实际上生成的代码给你们看
package com.dubbo.demo.provider.Demo;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class UserService$Adaptive implements com.dubbo.demo.provider.Demo.UserService {
public void login(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
}
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("user.service", "normal");
if (extName == null) {
throw new IllegalStateException(
"Fail to get extension(com.dubbo.demo.provider.Demo.UserService) name from url(" +
url.toString() + ") use keys([user.service])");
}
com.dubbo.demo.provider.Demo.UserService extension = (com.dubbo.demo.provider.Demo.UserService) ExtensionLoader.getExtensionLoader(com.dubbo.demo.provider.Demo.UserService.class)
.getExtension(extName);
extension.login(arg0);
}
}
是不是恍然大悟?相信读完上面的代码你们知道这是这个机制的实际原理了!
结语
这篇文章仅仅是讲解了 @Adaptive
的一个作用,没有特别高深的知识。下节我会将 Dubbo
服务暴露的内容
下期见!