今天小编工作上遇到一个关于dubbo泛化调用比较有代表性意义的坑,写出来给小伙伴们分享一下,希望大家能从我的经验中获得一些成长,未来遇到类似的场景也能少走一些弯路。
我们来看下dubbo框架中对泛化调用的接口定义:
public interface GenericService {
/**
* 泛化调用
*
* @param method 方法名,如:findPerson,如果有重载方法,需带上参数列表,如:findPerson(java.lang.String)
* @param parameterTypes 参数类型
* @param args 参数列表
* @return 返回值
* @throws Throwable 方法抛出的异常
*/
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
}
可以看到泛化调用,需要入参方法名,参数类型列表和参数值列表。
再来看段dubbo泛化调用的示例代码:
// 注册中心
RegistryConfig registry = new RegistryConfig();
registry.setAddress("127.0.0.1:2181");
registry.setProtocol("zookeeper");
// 应用信息
ApplicationConfig application = new ApplicationConfig();
application.setName("GenericInvokeTest");
application.setRegistry(registry);
// 泛化接口
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
// 真实接口
reference.setInterface("com.test.pdf.service.PdfConvertService");
// dubbo注册的版本号
reference.setVersion("1.0.0");
// 超时时间
reference.setTimeout(10000);
// 声明为泛化接口
reference.setGeneric(true);
reference.setApplication(application);
// 添加缓存策略
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
// 设置接口所需参数的类型
String[] parameterTypes = new String[]{"java.lang.String"};
// 传递 必传的参数
String request = "https://www.test.com/test.doc";
Object[] args0 = new Object[]{request};
//调用泛化接口
Object object = genericService.$invoke("word2Pdf", parameterTypes, args0);
System.out.println(JSON.toJSONString(object));
一般情况下,我们通过以上代码可以很好的实现dubbo远程泛化调用,一切看起来是那么的完美(当然了,上述示例仅使用了java原生数据类型,如果是自定义类型也是一样的,这里就不贴代码了,否则有凑字数的嫌疑)。
来看一个远端服务端真实接口定义,正常情况下是这样的:
/**
* 校验url访问权限
*
* @param loginUser 用户信息
* @param url 访问url
* @param type 访问类型
* @return
*/
Response<PrivilegeCheck> isResourcePermit(LoginUser loginUser, String url, String type);
小编特意在这里的将入参增加了一个自定义数据类型LoginUser,其定义如下:
public interface LoginUser {
Long getMainId();
Integer getStatus();
String getAccount();
}
小伙伴们可能发现了,这个isResourcePermit方法第一个入参怎么是个interface呢?其实正常情况入参是接口也没有关系,因为正常Dubbo invoke调用时是有传入真实参数类型的。已dubbo默认的Javassist代理来看:
public class JavassistProxyFactory extends AbstractProxyFactory {
@SuppressWarnings("unchecked")
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper类不能正确处理带$的类名
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
}
这里最终invokeMethod的时候传入的arguments就是真实的类信息。
回到泛化调用的问题上来,先上证据:
{
"methodName": "isResourcePermit",
"paramTypes": [
"com.test.user.dto.LoginUser",
"java.lang.String",
"java.lang.String"
],
"paramValues": [
{
"id": 21232,
"mainId": 21232,
"status": 1,
"account": "dcgg19899260300"
},
"/ad/exchange/msg/operators/event/messages",
"get"
]
}
这里我们构造了paramTypes和paramValues来匹配isResourcePermit接口。
请注意,这里问题就来了,paramTypes的第一个入参其实是个接口,但是paramValues入参一定是个具体的实现类对象。这里我们构造了LoginUser的实现类MyLoginUser,其定义如下:
@Data
public class MyLoginUser implements LoginUser {
private Long id;
private Long mainId;
private Integer status;
private String account;
}
仔细看MyLoginUser比LoginUser多了1个Long类型的id字段。我们来泛化调用的效果:
这里会报“java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long”。这就是所谓的dubbo泛化调用真实类型丢失问题。
仔细想一想,也确实是这样,我们泛化调用的只告诉了dubbo参数接口类型,反序列化时dubbo怎么会知道应该反序列化成什么对象呢!
明白了这个道理,我们不禁会想,难道dubbo框架没有考虑到这个问题吗?事实上,dubbo有考虑到这个问题,解决的思路也很简单,那就是在paramValues中指定实现类的类型。
[
{
"id": 21232,
"mainId": 21232,
"status": 1,
"account": "dcgg19899260300",
"class": "com.test.user.dto.MyLoginUser"
},
"/ad/exchange/msg/operators/event/messages",
"get"
]
使用以上paramValues我们就获取到了想要的结果。
{
"success":true,
"result":{
"accessType":0,
"isPermit":true,
},
"message":null,
}
附录:dubbo协议头信息
dubbo协议头是16字节的定长数据,16*8=128位,地址范围0~127。
2字节magic字符串0xdabb,0-7高位,8-15低位
1字节的消息标志位,16-20序列id,21 event,22 two way,23请求或响应标识
1字节状态,当消息类型为响应时,设置响应状态,24-31位
8字节,消息ID,long类型,32-95位
4字节,消息长度,96-127位
源代码参考:com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec
公众号链接:
深刻体会dubbo泛化调用类型丢失问题mp.weixin.qq.com