如何打造一个Dubbo网关--模拟网关

前文已经构建了一个完整的将dubbo服务转换为http+json的网关了。但还有一个重要的问题:开发人员如何在本地调试呢?我们假设如下条件

  1. 开发使用vpn接入dev环境,本地项目可以正常启动;但网关无法连接开发机器上的服务
  2. 开发阶段使用单元测试自测;但联调和提测阶段,前端和测试人员只会给接口名和参数,最多给一个curl请求
  3. 此时通过在dev环境里的日志可以解决还好,需要加日志甚至打断点怎么办?不断发布重启甚至开放调试端口吗?

为此我们需要一种可以在开发本地机器,按照网关格式调用本地dubbo服务的方案

本篇内容主要讲述本地调试,以下代码片段全部来自于plume

主要思路

  1. 使用Spring的@RestController实现一个GatewayController,uri以/run开头,剩余部分和网关一致
  2. 使用Spring的@RestControllerAdvice去处理这个controller的异常
  3. 获取接口的实现类的实例,这个简单,我们的实现类一定是接口名+Impl,只要按照SpringBoot的命名规则来即可
  4. 通过反射获取方法,然后需要通过某种手段把输入参数名和方法参数名对应起来,按照顺序排序成数组供方法调用,最后返回即可

MethodHandle

上面的123比较简单,直接从4开始。在这里使用了方法句柄:一种更加通用和灵活的动态执行方法的手段,比Method要轻的多,获取和使用都比Method方便,而且性能稍好

方法句柄和反射一样都有权限问题,但它是在lookup时检查权限的,也就是说可以将lookup定义在类从而可以使用私有方法。我们调用的都是接口的实现类,肯定不会有权限问题

  • getTarget:拿到spring容器里的bean,以及如果使用了aop将代理剥离后的原始bean。下文会说明为何要这样做
  • getMethod:使用spring的ParameterNameDiscoverer机制拿到方法的相关信息,主要是参数名和参数类型
  • sortInputByMethod:根据上两步的信息,将输入排序并转为数组形式的入参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private Object handle(String proof, String tenant, String clazz, String method, Map<String, Object> param) {
setRpcContext(proof, tenant);
final PairTuple<Object, Object> targetInfo = getTarget(clazz);
final ThirdTuple<List<String>, Parameter[], MethodType> methodInfo = getMethod(targetInfo.getLast(), method, param.size());

try {
//获取方法句柄
final MethodHandles.Lookup lookup = MethodHandles.lookup();
final MethodHandle handle = lookup.bind(targetInfo.getFirst(), method, methodInfo.getLast());
final Object[] objects = ApiTestUtil.sortInputByMethod(param, methodInfo.getFirst(), methodInfo.getMiddle());

return handle.invokeWithArguments(objects);
} catch (Throwable ex) {
if (ex instanceof InnerException) {
log.warn("[PLATFORM] 系统内部异常: ", ex);
throw new InnerException(ex.getMessage());
} else if (ex instanceof PassedException) {
log.info("[PLATFORM] 业务内部校验不通过: {}", ex.getMessage());
throw new InnerException(ex.getMessage());
} else if (ex instanceof RefundException) {
log.info("[PLATFORM] 调用了未授权的资源: {}", ex.getMessage());
throw new InnerException(ex.getMessage());
} else if (ex instanceof RpcException) {
log.error("[PLATFORM] DUBBO调用异常: ", ex);
throw new InnerException(PlatformExceptionEnum.CLIENT_TIMEOUT.getMessage());
} else if (ex instanceof IOException) {
log.error("[PLATFORM] IO使用异常: ", ex);
throw new InnerException(PlatformExceptionEnum.BAD_REQUEST.getMessage());
} else {
log.warn("[PLATFORM] 调用方法未知异常: ", ex);
throw new InnerException(ex.getMessage());
}
}
}

ParameterNameDiscoverer

众所周知,java8反射里是不会有参数名信息的,因为这些信息对方法调用是无效的,虽然可以通过编译参数添加但兼容性太差

但是对于spring来说controller的方法参数名可能要绑定啊,为此spring提供了LocalVariableTableParameterNameDiscoverer来获取方法的参数名

LocalVariableTableParameterNameDiscoverer的原理就是通过asm读取字节码,从LocalVariableTable中提取参数名

LocalVariableTable顾名思义:局部变量表。其只包含在方法代码块里,所以接口方法、抽象方法都是没有LocalVariableTable的,也就无法拿到参数名

另外注意LocalVariableTable属于调试信息,使用javac时要加上-g参数告诉编译器我们需要调试信息,使用maven时会自动添加该参数

getTarget

getTarget首先要从spring容器里根据beanName拿到其实例,随后调用AopTargetUtil.getTarget取得代理前的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
private PairTuple<Object, Object> getTarget(String clazz) {
try {
//获取spring中的bean
if (!clazz.endsWith(implSuffix)) {
clazz += implSuffix;
}
final Object bean = context.getBean(clazz);
return new PairTuple<>(bean, AopTargetUtil.getTarget(bean));
} catch (Exception ex) {
log.info("[PLATFORM] 未找到输入的服务: {}, {}", clazz, ex.getMessage());
throw new PassedException("未找到输入的服务");
}
}

AopTargetUtil.getTarget就是使用spring的AopUtils判断是否时aop代理对象,如果是要获得代理前的实例

之所以这样做是因为代理对象动态生成的字节码里不会包含这些无用的调试信息,接口里上面分析过同样也没有。为了LocalVariableTableParameterNameDiscoverer生效必须拿到真实的被代理之前的对象

1
2
3
4
5
6
7
8
9
10
11
12
public static Object getTarget(Object proxy) throws Exception {
//不是代理对象
if (!AopUtils.isAopProxy(proxy)) {
return proxy;
}

if (AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else {
return getCglibProxyTargetObject(proxy);
}
}

getMethod

LocalVariableTableParameterNameDiscoverer需要反射拿到Method,这里只能拿到参数长度所以无法使用getDeclaredMethod,所以这里也对接口的重载做出了限制:参数类型无用,不能出现方法名和参数长度都一样的方法

MethodType说明了方法句柄的一个重要性质:只和参数类型和返回值类型有关。在拿到这些数据后,将MethodType和aop代理后实例绑定到方法句柄上就调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private ThirdTuple<List<String>, Parameter[], MethodType> getMethod(Object target, String methodName, int inputParamSize) {
final Class<?> clazz = target.getClass();
log.info("[PLATFORM] 调用目标: {}, 方法名: {}, 输入参数长度: {}", target.getClass(), methodName, inputParamSize);

//获取spring中的方法参数名
for (Method method : clazz.getDeclaredMethods()) {
if (method.getName().equals(methodName)) {
final List<String> paramNames = new ArrayList<>();
final String[] discovererParamNames = discoverer.getParameterNames(method);

int discovererParamSize = 0;
if (null != discovererParamNames) {
Collections.addAll(paramNames, discovererParamNames);
discovererParamSize = discovererParamNames.length;
}
log.info("[PLATFORM] 查找到合适的方法: {}, 方法参数长度: {}", method, discovererParamSize);

final Parameter[] parameters = method.getParameters();
if (discovererParamSize == inputParamSize) {
List<Class<?>> parameterTypeList = Arrays.stream(parameters).map(Parameter::getType)
.collect(Collectors.toList());
return new ThirdTuple<>(paramNames, parameters, MethodType.methodType(method.getReturnType(), parameterTypeList));
}
}
}

throw new PassedException("未找到输入的方法");
}

如何打造一个Dubbo网关--模拟网关
https://back.pub/post/hh-code-dubbo-gateway-10/
作者
Dash
发布于
2019年3月30日
许可协议