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包装一下。
总结
从功能上来看,泛化调用提供了在没有接口依赖情况下进行的解决方案,丰富框架的使用场景。
从设计上来看,泛化调用的功能还是通过扩展的方式实现的,侵入性不强,值得学习借鉴。
以上就是dubbo泛化调用使用及原理示例解析的详细内容,更多关于dubbo泛化调用的资料请关注脚本之家其它相关文章!
相关文章
springboot返回值转成JSONString的处理方式
这篇文章主要介绍了springboot返回值转成JSONString的处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-06-06Spring Boot 使用观察者模式实现实时库存管理的步骤
在现代软件开发中,实时数据处理非常关键,本文提供了一个使用SpringBoot和观察者模式开发实时库存管理系统的详细教程,步骤包括创建项目、定义实体类、实现观察者模式、集成Spring框架、创建RESTful API端点和测试应用等,这将有助于开发者构建能够即时响应库存变化的系统2024-09-09
最新评论