没有接口,如何进行RPC调用?
Posted 乐信技术精英社
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了没有接口,如何进行RPC调用?相关的知识,希望对你有一定的参考价值。
导读
作者 | miloyuan(袁玮鸿)
导读
结合乐信服务框架【LSF】封装的【Dubbo】的基础特性之泛化调用,我们在此基础上进行适配业务层的进一步包装,目标是整洁、简化,方便业务使用。此篇幅对泛化原理进行了深入浅出的解析,以及在我们公司,大致有哪些场景用到了泛化流程?这里将一一揭秘。
引申
说到泛化,可能很多人可能会想到泛型,即为参数化类型,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法,它是JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。为什么要引入泛型?假设在没有泛型的情况的下,我们会对Object的对象在预知的情况下进行强制转换,而此类转换主流的编译器均不会提示异常,只有在运行时才可能出现异常,存在风险隐患。泛型则没有以上的缺点。
同时,也可能会联想到在UML类图中,常见的有以下几种关系: 泛化(Generalization),实现(Realization),关联(Association),聚合(Aggregation),组合(Composition),依赖(Dependency)中的泛化关系,它一种继承关系,表示一般与特殊的关系,指定了子类如何特化父类的所有特征和行为。
UML中用带箭头的实线表示,如下:
WHAT?
本文中讲的泛化主要是RPC泛化,可以理解是一个泛化调用动作,由具体具像化,到抽象通用规则的动作实现。可以先简单理解为就是没有了具体的模型对象,统一使用容器对象Map等来封装对象。我们常常看到一句话是说泛化调用是网关的基石?原因之一也在于此,使用者在调用提供者接口时,不再需要依赖服务提供方客户端的JAR包,因此也就没有了POJO,通过泛化的方式进行远程调用。
一般情况下我们需要通过RPC调用接口提供方的服务,首先在消费端嵌入提供方的JAR包,从而使用JAR包中的类和方法。那对于应用系统来说,如果一个前置服务调用了N个服务,那就需要引入N个JAR包依赖,那这样就形成了如下网络经脉图:
以上关系图,如果是一般的业务应用系统,那还不算是问题,因为对于业务应用系统来说,可能存在同一个服务调用不同的服务场景不多见,那如果是类似网关应用系统的话,每接入一个新服务就会添加一个JAR包以及接口适配,那简直是不忍直视。有没有一种方式能解决这个问题呢?答案就是泛化调用,这种方式不在需要服务提供方提供JAR包便可完成RPC调用,其中的原理其实跟普通的RPC调用时一致的,网络、序列化、反射这些底层的技术原理一致。区别在于参数和返回值都用容器对象Map等来表示,通过泛化服务来调用所有的服务实现。目前,几乎主流的RPC框架都会支持泛化调用,比如阿里的HSF框架、Dubbo框架等等。
使用泛化调用的网关系统只需要继承RPC框架基础的一个JAR包即可,其余的接口都通过泛化来调用服务封装的能力来实现,这样无论网关系统承载多少个接口,都来多少接收多少,结构如下:
以上图可以看出,泛化调用取代了多JAR调用方式的集大成者,它仍然是在基于RPC底层通信的基础之上进行调用的,跟普通RPC区别在于不在依赖多个服务提供方客户端JAR包了,入参和出参就完全用容器对象Map来代替。泛化在RPC中出现的概念来于此,从具体到抽象,不在需要具体的对象属性领域模型。
HOW?
它是如何做到的呢?
我们知道,在 RPC 调用的过程中,调用端向服务端发起请求,首先要通过动态代理,动态代理可以帮助我们屏蔽 RPC 处理流程,真正地让我们发起远程调用就像调用本地一样。那么在 RPC 调用的过程中,既然调用端是通过动态代理向服务端发起远程调用的,那么在调用端的程序中就一定要依赖服务提供方提供的接口 API,因为调用端是通过这个接口 API 自动生成动态代理的。那如果没有接口 API 我们该如何让调用端仍然能够发起 RPC 调用呢?
所谓的 RPC 调用,本质上就是调用端向服务端发送一条请求消息,服务端接收并处理,之后向调用端发送一条响应消息,调用端处理完响应消息之后,一次 RPC 调用就完成了。那是不是说我们只要能够让调用端在没有服务提供方提供接口的情况下,仍然能够向服务端发送正确的请求消息,就能够解决这个问题了呢?
没错,只要调用端将服务端需要知道的信息,如接口名、业务分组名、方法名以及参数信息等封装成请求消息发送给服务端,服务端就能够解析并处理这条请求消息,这样问题就解决了。过程如下图所示:
现在我们已经清楚了解决问题的关键,但 RPC 的调用端向服务端发送消息是需要以动态代理作为入口的,为了让调用端发送刚才讲过的那条请求消息。以Dubbo为例官方统一的接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,而 GenericService 接口的 invoke方法的入参就是方法名以及参数信息。这样我们传递给服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等都可以通过调用GenericService代理的invoke 方法来传递。具体的接口定义如下:
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
}
这个通过统一的 GenericService 接口类生成的动态代理,来实现在没有接口的情况下进行 RPC 调用的功能,我们就称之为泛化调用。
应用场景有哪些?
在实践的过程中,让调用端在没有接口 API 的情况下发起 RPC 调用的需求,有哪些经典的案例呢?请看下面的应用场景。
场景一:比如架构的扁鹊测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。这时就有一个问题要解决,搭建统一的测试平台实际上是作为各个 RPC 服务的调用端,而在 RPC 框架的使用中,调用端是需要依赖服务提供方提供的接口 API 的,而统一测试平台不可能依赖所有服务提供方的接口 API。我们不能因为每有一个新的服务发布,就去修改平台的代码以及重新上线。这时就需要让调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。
场景二:比如业务接入组搭建的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。这时就有与场景一相同的问题,服务网关要作为所有 RPC 服务的调用端,是不能依赖所有服务提供方的接口 API 的,也需要调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起 RPC 调用。示意图这两个场景都是我们经常会碰到的,而让调用端在没有服务提供方提供接口 API 的情况下仍然可以发起 RPC 调用的功能,在 RPC 框架中也是非常有价值的。
在实际的API网关中我们如何操作?
网关系统与RPC环境起初是不同环境的事务系统,他们互不依赖,各有各的生命周期。在RPC环境下,调用者编写好各自的业务逻辑代码,通过提供者的客户端JAR包调用提供者的服务,注册中心负责同步数据,这个时候网关系统跟RPC服务是没有联系的。如下图所示。
那我们是如何把API接口发布到网关系统中呢?实际问题是需要利用什么方法将RPC环境下的API接口让网关也能识别识别到,剩余的工作还是交给RPC本身去完成,包括编解码、序列化、反序列化、长连接等。有了泛化调用作为基础支持,我们需要做的就是将API通过一种方式存储到网关系统能够访问的一种存储中,为了提高系统的性能一般会选用Redis存储。根据泛化调用的方式,网关系统需要知道服务的类名和方法名。网关系统可以提供一个API发布平台入口,让API发布者将RPC环境下的API数据录入到API发布平台。RPC本身就可以为官网系统提供一个获取API信息的接口,API发布平台可以在用户输入服务的类名之后直接通过这个接口获取要发布的API整体信息,包括所有的方法、入参、出参、注释、描述、接口负责人信息等。剩余的工作就可以交给API网关的泛化调用逻辑了,如下图:
原理与实践
我们知道,正常的RPC调用是由服务提供者与服务调用者组成。那么RPC泛化也是同样的道理,它分为泛化实现与泛化调用,泛化实现在业务的应用场景比较少见,而泛化调用算是比较常见的,这里我们重点解析泛化调用的场景。以Dubbo官方为例,对泛化调用的定义如下:
泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用
Map
表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过GenericService
调用所有服务实现。
从描述可以看出泛化调用的重点在于提供一个通用的服务。这样的服务可能没有接口的定义,没有明确的方法列表,通过一个通用流程来实现业务处理。
泛化调用
即是服务调用者,对于Dubbo来说,通常是服务调用方没有引入JAR包,也就不包含接口中的实体类,故服务调用方只能提供容器对象形式的数据,由服务提供者根据容器对象转化成对应的实体。
泛化实现大致有两种方式:
其一、通过 Spring 使用泛化调用
<dubbo:reference id="orderESSynService" interface="com.fenqile.order.order_search.service.OrderESSynService"
protocol="fsof" group="default" version="1.0.0" check="false" timeout="3000" generic="true"/>
其二、通过 API 方式使用泛化调用
// 引用远程服务
// 该实例很重量,里面封装了所有与注册中心及服务提供方连接,请缓存
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
// 弱类型接口名
reference.setInterface("com.fenqile.order.order_search.service.OrderESSynService");
reference.setVersion("1.0.0");
// 声明为泛化接口
reference.setGeneric(true);
// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用
GenericService genericService = reference.get();
// 用Map表示POJO参数,如果返回值为POJO也将自动转成Map
Map<String, Object> req = new HashMap<String, Object>();
req.put("name", "O20190226443864100225");
// 如果返回POJO将自动转成Map
Object result = genericService.$invoke("findPerson", new String[]
{"com.fenqile.order.order_search.dto.OrderESSynRequest"}, new Object[]{req});
其底层的泛化调用源码是:
//@Activate(group = CommonConstants.CONSUMER, value = GENERIC_KEY, order = 20000)
public class GenericImplFilter extends ListenableFilter {
private static final Class<?>[] GENERIC_PARAMETER_TYPES =
new Class<?>[]{String.class, String[].class, Object[].class};
public GenericImplFilter() {
super.listener = new GenericImplListener();
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String generic = invoker.getUrl().getParameter(GENERIC_KEY);
//服务端是泛化暴露,客户端不是使用泛化调用场景
if (ProtocolUtils.isGeneric(generic)
&& (!$INVOKE.equals(invocation.getMethodName()) && !$INVOKE_ASYNC.equals(invocation.getMethodName()))
&& invocation instanceof RpcInvocation) {
RpcInvocation invocation2 = new RpcInvocation(invocation);
String methodName = invocation2.getMethodName();
Class<?>[] parameterTypes = invocation2.getParameterTypes();
Object[] arguments = invocation2.getArguments();
String[] types = new String[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
types[i] = ReflectUtils.getName(parameterTypes[i]);
}
Object[] args;
// 客户端(非泛化)到服务端(泛化)根据不同协议进行序列化
if (ProtocolUtils.isBeanGenericSerialization(generic)) {
args = new Object[arguments.length];
for (int i = 0; i < arguments.length; i++) {
args[i] = JavaBeanSerializeUtil.serialize(arguments[i], JavaBeanAccessor.METHOD);
}
} else {
args = PojoUtils.generalize(arguments);
}
if (RpcUtils.isReturnTypeFuture(invocation)) {
invocation2.setMethodName($INVOKE_ASYNC);
} else {
invocation2.setMethodName($INVOKE);
}
invocation2.setParameterTypes(GENERIC_PARAMETER_TYPES);
invocation2.setArguments(new Object[]{methodName, types, args});
// 客户端调用转换为服务端的泛化调用
return invoker.invoke(invocation2);
// 服务端非泛化暴露,消费使用泛化调用
} else if ((invocation.getMethodName().equals($INVOKE) || invocation.getMethodName().equals($INVOKE_ASYNC))
&& invocation.getArguments() != null
&& invocation.getArguments().length == 3
&& ProtocolUtils.isGeneric(generic)) {
Object[] args = (Object[]) invocation.getArguments()[2];
// 校验不同的序列化格式是否正确
if (ProtocolUtils.isJavaGenericSerialization(generic)) {
for (Object arg : args) {
if (!(byte[].class == arg.getClass())) {
error(generic, byte[].class.getName(), arg.getClass().getName());
}
}
} else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
for (Object arg : args) {
if (!(arg instanceof JavaBeanDescriptor)) {
error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
}
}
}
invocation.setAttachment(
GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));
}
return invoker.invoke(invocation);
}
}
以上源码可以从以上几点去理解,其一是判断是否是泛化服务,其二为泛化服务的序列化,其三为根据反射机制获取对应的处理方法。
推荐实践
基于以上的原理以及RPC调用接入方式,考虑到实际的业务使用习惯,我们在乐信基础框架lexin_common的0.0.1版本的RPC模块,对强大的Dubbo的泛化进行了进一步友好简洁包装。主要是通过本地化缓存泛化调用比较重的连接注册中心以及服务提供方的处理方式,此外,还有对主要的参数进行的封装。主要源码见下:
/**
* <Li>泛化调用duubo远程接口</Li>
*
* @param config 服务配置
* @param method 方法名
* @param args 参数数组
* @return
*/
public static Object invoke(DubboBo dubboBo, String method, Object[] args) {
ApplicationConfig applicationConfig = SpringContextFacade.getBean(ApplicationConfig.class);
RegistryConfig registryConfig = SpringContextFacade.getBean(RegistryConfig.class);
// 组装
String group = StringUtils.isBlank(dubboBo.getGroup()) ? "default" : dubboBo.getGroup();
String version = StringUtils.isBlank(dubboBo.getVersion()) ? "1.0.0" : dubboBo.getVersion();
String key = "KEY_" + dubboBo.getInterfaceName() + group + dubboBo.getVersion();
if (referenceCache.get(key) == null) {
// 该实例很重量,里面封装了所有与注册中心及服务提供方连接
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig);
// 弱类型接口名
reference.setInterface(dubboBo.getInterfaceName());
reference.setTimeout(dubboBo.getTimeout() == null ? 3000 : dubboBo.getTimeout());
reference.setGroup(group);
reference.setVersion(version);
reference.setUrl(dubboBo.getUrl());
// 声明为泛化接口
reference.setGeneric(true);
referenceCache.put(key, reference);
}
// 入参类型构造
int len = args.length;
String[] invokeParamTypes = new String[len];
for (int i = 0; i < len; i++) {
invokeParamTypes[i] = args[i].getClass().toString();
}
// 执行
GenericService genericService = ReferenceConfigCache.getCache().get(referenceCache.get(key));
return invoke(genericService, method, invokeParamTypes, args);
}
在使用侧,可分别支持通过 Spring 使用泛化调用与通过 API方式使用泛化调用,只需填写一些必要的接口路径即可,以下是API方式的测试接入例子:
/**
* <Li>泛化调用duubo远程接口</Li>
*/
@Test
public void test() {
Map<String, Object> params = Maps.newHashMap();
params.put("type", "BT");
params.put("num", 1);
params.put("zeroType", 1);
DubboBo dubboBo = new DubboBo();
dubboBo.setInterfaceName("com.fenqile.inbiz.seqno.SeqNoProtocol");
Object object = DubboFacade.invoke(dubboBo, "getSeqNo", params);
Assert.assertNotNull(object);
}
总结
通过如何在没有接口的情况下进行 RPC 调用,引导出泛化调用并阐述其原理,核心原理为通过调用 GenericService 代理的 $invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 RPC 调用的功能。并且通过解析应用场景对原理进行了进一步剖析,最后基于Dubbo泛化的基础上提供工具类的推荐实践。
end
热门文章:
以上是关于没有接口,如何进行RPC调用?的主要内容,如果未能解决你的问题,请参考以下文章
jmeter3.2版本如何进行webservice接口功能测试