dubbo泛化调用使用及原理解析
本文案例代码见 git@github.com:shengchaojie/dubbo_best_practise.git
什么是泛化调用
通常我们想调用别人的dubbo服务时,我们需要在项目中引入对应的jar包。而泛化调用的作用是,我们无需依赖相关jar包,也能调用到该服务。
这个特性一般使用在网关类项目中,在业务开发中基本不会使用。
使用方式
假设我现在要调用下面的接口服务
package com.scj.demo.dubbo.provider.service.impl;
public interface ByeService {
String bye(String name);
}
api
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setApplication(new ApplicationConfig("test"));
referenceConfig.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));
referenceConfig.setInterface("com.scj.demo.dubbo.provider.service.impl.ByeService");
referenceConfig.setGeneric(true);
GenericService genericService = referenceConfig.get();
Object result = genericService.$invoke(
"bye",
new String[]{"java.lang.String"},
new Object[]{"1234"});
System.out.println(result);
spring
在xml文件做以下配置
<dubbo:reference id="byeService" interface="com.scj.demo.dubbo.provider.service.impl.ByeService" generic="true" />
然后注入使用
@Service
public class PersonService {
@Resource(name = "byeService")
private GenericService genericService;
public void sayBye(){
Object result = genericService.$invoke(
"bye",
new String[]{"java.lang.String"},
new Object[]{"1234"});
System.out.println(result);
}
}
在两种调用方式中,我们都需要使用被调用接口的字符串参数生成GenericService,通过GenericService的$invoke间接调用目标接口的接口。
public interface GenericService {
/**
* Generic invocation
*
* @param method Method name, e.g. findPerson. If there are overridden methods, parameter info is
* required, e.g. findPerson(java.lang.String)
* @param parameterTypes Parameter types
* @param args Arguments
* @return invocation return value
* @throws Throwable potential exception thrown from the invocation
*/
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
}
$invoke的三个参数分别为,方法名,方法参数类型数组,方法参数数组。
方法入参构造
可以看到泛化调用的一个复杂性在于$invoke的第三个参数的组装,下面介绍几种复杂入参的调用方式
首先丰富提供者接口
public interface ByeService {
String bye(String name);
String bye(String name, Long age, Date date);
String bye(Person person);
String bye(List<String> names);
String bye(String[] names);
String byePersons(List<Person> persons);
String byePersons(Person[] persons);
@Data
public static class Person{
private String name;
private Long age;
private Date birth;
}
public static void main(String[] args) {
System.out.println(Person.class.getName());
}
}
多参数
@Test
public void testMultiParam(){
result = genericService.$invoke(
"bye",
new String[]{"java.lang.String","java.lang.Long","java.util.Date"},
new Object[]{"scj",12L,new Date()});
System.out.println(result);
}
POJO
Map<String,Object> personMap = new HashMap<>();
{
personMap.put("name","scj");
personMap.put("age","12");
personMap.put("birth",new Date());
}
@Test
public void testPOJO(){
result = genericService.$invoke(
"bye",
new String[]{"com.scj.demo.dubbo.provider.service.impl.ByeService$Person"},
new Object[]{personMap});
System.out.println(result);
}
Map
@Test
public void testMap(){
result = genericService.$invoke(
"bye",
new String[]{"java.util.Map"},
new Object[]{personMap});
System.out.println(result);
}
集合
@Test
public void testList(){
List<String> names = Lists.newArrayList("scj1","scj2");
result = genericService.$invoke(
"bye",
new String[]{"java.util.List"},
new Object[]{names});
System.out.println(result);
}
数组
@Test
public void testArray(){
String[] nameArray = new String[]{"scj1","scj3"};
result = genericService.$invoke(
"bye",
new String[]{"java.lang.String[]"},
new Object[]{nameArray});
System.out.println(result);
}
集合+POJO
@Test
public void testPOJOList(){
result = genericService.$invoke(
"byePersons",
new String[]{"java.util.List"},
new Object[]{Lists.newArrayList(personMap,personMap)});
System.out.println(result);
}
数组+POJO
@Test
public void testPOJOArray(){
result = genericService.$invoke(
"byePersons",
new String[]{"com.scj.demo.dubbo.provider.service.impl.ByeService$Person[]"},
new Object[]{Lists.newArrayList(personMap,personMap)});
System.out.println(result);
}
结果返回
与入参相似,虽然$invoke的返回定义为Object,实际上针对不同类型有不同的返回。
别想着转换为POJO,你都泛化调用了,搞不到接口,如何转换。当然自己定义一个完全一样的当然也行。
接口返回类型 | $invoke返回类型 |
---|---|
基础类型 | 基础类型 |
POJO | HashMap |
Collection | List返回ArrayList,Set返回HashSet |
Array | Array |
组合类型 | 根据上述映射组合返回 |
原理介绍
消费者端
泛化调用和直接调用在消费者者端,在使用上的区别是,我们调用服务时使用的接口为GenericService,方法为$invoker。在底层的区别是,消费者端发出的rpc报文发生了变化。
使用方式上的改变
在使用上,不管哪种配置方式,我们都需要配置generic=true
设置generic=true后,RefereceConfig的interfaceClass会被强制设置为GenericService
if (ProtocolUtils.isGeneric(getGeneric())) {
//如果是泛化调用
interfaceClass = GenericService.class;
} else {
try {
interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
checkInterfaceAndMethods(interfaceClass, methods);
}
这也使得我们的RefereanceBean返回的是GenericService类型的代理。
invoker = refprotocol.refer(interfaceClass, urls.get(0));
生成的代理是GenericService的代理只是我们使用方式上的变化,更为核心的是,底层发送的rpc报文发生了什么变化。
底层报文变化
Dubbo的rpc报文分为header和body两部分。我们这边只需要关注body部分。构造逻辑如下
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);//dubbo版本号
out.writeUTF(inv.getAttachment(Constants.PATH_KEY));//path 就是接口全限定名
out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));// 接口版本号
out.writeUTF(inv.getMethodName());//方法名
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));//方法参数类型
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));//方法参数
}
}
out.writeObject(RpcUtils.getNecessaryAttachments(inv));//rpc上下文
}
那么我们通过直接调用与泛化调用ByeService的bye方法在报文上有啥区别呢?
我一开始以为报文中的path是GenericeService,其实并没有,path就是我们调用的目标方法。
path来源???todo
而报文中的方法名,方法参数类型以及具体参数,还是按照GenericeService的$invoke方法入参传递的。
这么个二合一的报文,发送到提供者那边,它估计也会很懵逼,我应该怎么执行?
所以针对泛化调用报文还会把generic=true放在attchment中传递过去
具体逻辑在GenericImplFilter中。
GenericImplFilter中有很多其他逻辑,比如泛化调用使用的序列化协议,正常接口走泛化调用的模式,我们只需要设置attachment的那部分。
针对泛化调用,要进行2次序列化/反序列化。看下POJO的调用方式你就知道为啥了
((RpcInvocation) invocation).setAttachment(
Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));
知道消费者端报文发生了什么变化,那么接下来就去看提供者端如何处理这个改造后的报文。
interfaceClass和interfaceName的区别
总结一下ReferenceConfig中interfaceClass和interfaceName的区别?(这道面试题好像不错)
interfaceClass用于指定生成代理的接口
interfaceName用于指定发送rpc报文中的path(告诉服务端我要调用那个服务)
提供者端
消费者泛化调用的rpc报文传递到提供者还不能直接使用,虽然path是对的,但是实际的方法名,参数类型,参数要从rpc报文的参数中提取出来。
GenericFilter就是用来做这件事情。
在提供者这边,针对泛化调用的逻辑全部封装到了GenericFilter,解耦的非常好。
GenericFilter逻辑分析
1. 是否是泛化调用判断
if (inv.getMethodName().equals(Constants.$INVOKE)
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())){
//...
}
注意第4个条件,一开始很疑惑,后来发现rpc报文中的path是目标接口的,这边invoker.getInterface()返回的肯定就是实际接口了
2. 方法参数提取
//从argument提取目标方法名 方法类型 方法参数
String name = ((String) inv.getArguments()[0]).trim();
String[] types = (String[]) inv.getArguments()[1];
Object[] args = (Object[]) inv.getArguments()[2];
3. 方法参数解析,进一步反序列化
//反射获取目标执行方法
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class<?>[] params = method.getParameterTypes();
if (args == null) {
args = new Object[params.length];
}
String generic = inv.getAttachment(Constants.GENERIC_KEY);
if (StringUtils.isBlank(generic)) {
generic = RpcContext.getContext().getAttachment(Constants.GENERIC_KEY);
}
//一些反序列化
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)) {
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
} else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (byte[].class == args[i].getClass()) {
try {
UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]);
args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
.getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
.deserialize(null, is).readObject();
} catch (Exception e) {
throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
}
} else {
throw new RpcException(
"Generic serialization [" +
Constants.GENERIC_SERIALIZATION_NATIVE_JAVA +
"] only support message type " +
byte[].class +
" and your message type is " +
args[i].getClass());
}
}
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof JavaBeanDescriptor) {
args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
} else {
throw new RpcException(
"Generic serialization [" +
Constants.GENERIC_SERIALIZATION_BEAN +
"] only support message type " +
JavaBeanDescriptor.class.getName() +
" and your message type is " +
args[i].getClass().getName());
}
}
}
这边有个疑问,为什么这边还要再次反序列化一次,netty不是有decoder么??
嗯,你别忘了,针对一个POJO你传过来是一个Map,从Map转换为POJO需要这边进一步处理。
4. 调用目标服务
Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
这边的invoker就是实际服务提供者的invoker,因为我们的path是正确的,invoker获取在DubboProtocl的requestHandler回调中
5. 异常处理
if (result.hasException()
&& !(result.getException() instanceof GenericException)) {
return new RpcResult(new GenericException(result.getException()));
}
这边需要注意一下!!针对接口的泛化调用,抛出的异常都会经过GenericException包装一下。
总结
从功能上来看,泛化调用提供了在没有接口依赖情况下进行的解决方案,丰富框架的使用场景。
从设计上来看,泛化调用的功能还是通过扩展的方式实现的,侵入性不强,值得学习借鉴。