一、概念镇场子,场景知其然
1.什么是泛化调用?
泛化调用是指在调用方没有服务方提供的API(SDK)的情况下,对服务方进行调用,并且可以拿到调用结果。
2.什么时候会用到泛化调用?
测试集成平台
我们要搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的RPC服务。这时我们就有一个问题要解决,我们搭建统一的测试平台实际上是作为各个RPC服务的调用端,而在RPC框架的使用中,调用端是需要依赖服务提供方提供的接口API的,而统一测试平台不可能依赖所有服务提供方的接口API。我们不能因为每有一个新的服务发布,就去修改平台的代码以及重新上线。这时我们就需要让调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起RPC调用。
网关服务
我们要搭建一个轻量级的服务网关,可以让各个业务方用HTTP的方式,通过服务网关调用其它服务。这时就有与场景一相同的问题,服务网关要作为所有RPC服务的调用端,是不能依赖所有服务提供方的接口API的,也需要调用端在没有服务提供方提供接口的情况下,仍然可以正常地发起RPC调用。
3.目前市面上都有哪些熟知的框架使用泛化调用
是个RPC框架都会使用。外部主要代表有:阿里的Dubbo【一搜泛化调用立马蹦出来的就是它】;某公司内部:Thrift。
PS:这几年吵得很火的Serverless也会使用
二、小试牛刀,上手撸
1.某公司Thrift为例子
拉横幅:使用此功能,建议服务端和调用端使用thrift 制定版本以上
【服务方】(无需改动,正常配置即可)
Provider的XML方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="serviceProcessor" class="com.xxx.GenericImpl">
</bean>
<bean id="serverPublisher" class="com.xxx.ThriftServerPublisher"
init-method="publish" destroy-method="destroy">
<property name="appKey" value="com.xxx.benchmark"/>
<property name="port" value="9998"/>
<property name="serviceInterface" value="com.xxx.Generic"/>
<property name="serviceImpl" ref="serviceProcessor"/>
</bean>
</beans>
Provider的实现
public class GenericImpl implements Generic.Iface {
private static final Logger logger = LoggerFactory.getLogger(GenericImpl.class);
@Override
public void echo1() throws TException {
logger.info("echo1");
}
@Override
public String echo2(String message) throws TException {
logger.info("echo2");
return message;
}
@Override
public SubMessage echo3(SubMessage message) throws TException {
logger.info("echo3");
return message;
}
}
【调用方配置】
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="clientProxy" class="com.xxx.ThriftClientProxy" destroy-method="destroy">
<property name="timeout" value="1000"/>
<property name="appKey" value="xxx"/>
<property name="remoteAppkey" value="com.xxx.benchmark"/>
<!-- 实际的服务接口名 -->
<property name="genericServiceName" value="com.xxx.Generic"/>
<property name="remoteServerPort" value="9998"/>
<!-- 目前只支持json、json-common、json-simple -->
<property name="generic" value="json"/>
</bean>
</beans>
public class GenericClient {
private static ClassPathXmlApplicationContext clientBeanFactory;
private static GenericService client;
public static void main(String[] args) throws TException {
clientBeanFactory = new ClassPathXmlApplicationContext("generic.xml");
client = clientBeanFactory.getBean(GenericService.class);
testEcho1();
testEcho2();
testEcho3();
clientBeanFactory.destroy();
System.exit(0);
}
}
方式二:API方式
//声明
ThriftClientProxy clientProxy = new ThriftClientProxy();
clientProxy.setAppKey("com.xxx.Client");
clientProxy.setRemoteAppkey("com.xxx.benchmark");
clientProxy.setGenericServiceName("com.xxx.Generic");
clientProxy.setRemoteServerPort(9998);
clientProxy.setFilterByServiceName(true);
clientProxy.setGeneric("json");
clientProxy.afterPropertiesSet();//触发初始化
GenericService genericClient = clientProxy.getObject();
//echo1
List<String> paramTypes = new ArrayList<String>();
List<String> paramValues = new ArrayList<String>();
String result = genericService.$invoke("echo1", paramTypes, paramValues);
System.out.println(result);
//echo2
List<String> paramTypes = new ArrayList<String>();
paramTypes.add("java.lang.String");
List<String> paramValues = new ArrayList<String>();
String expected = JacksonUtils.serialize("hello world");
paramValues.add(expected);
String result = genericService.$invoke("echo2", paramTypes, paramValues);
System.out.println(result);
assert(expected.equals(result));
//echo3
List<String> paramTypes = new ArrayList<String>();
paramTypes.add("com.xxx.SubMessage");
List<String> paramValues = new ArrayList<String>();
SubMessage subMessage = new SubMessage();
subMessage.setId(1);
subMessage.setValue("hello world");
String expected = JacksonUtils.serialize(subMessage);
paramValues.add(expected);
String result = genericService.$invoke("echo3", paramTypes, paramValues);
System.out.println(result);
assert (expected.equals(result));
【解释分析】
调用方发送的数据和服务端返回的数据格式是在json格式的基础上做了定制。
具体用法:
在发送前将参数用JacksonUtils的序列化方法进行序列化,
返回的结果用JacksonUtils的解序列化方法即可,具体参考下文的示例
可选项 | 序列化方法 | 说明 | 注意 |
---|---|---|---|
json | JacksonUtils.serialize JacksonUtils.deserialize | 服务端返回定制化的json,客户端需要传递定制化的json | |
json-common | JacksonUtils.serialize JacksonUtils.simpleDeserialize | 服务端返回普通的json,客户端需要传递定制化的json | 普通的json可以忽略setXX字段,但是:若自动生成类的字段是private(编译IDL时使用了private-members),将无法忽略 |
json-simple | JacksonUtils.serialize JacksonUtils.simpleDeserialize | 服务端返回普通的json,客户端只需传递普通的json | 同上 |
2.外部阿里为例子
Provider端
public class DemoServiceImpl implements DemoService {
public List<String> getPermissions(Long id) {
List<String> demo = new ArrayList<String>();
demo.add(String.format("Permission_%d", id - 1));
demo.add(String.format("Permission_%d", id));
demo.add(String.format("Permission_%d", id + 1));
return demo;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--定义了提供方应用信息,用于计算依赖关系;在 dubbo-admin 或 dubbo-monitor 会显示这个名字,方便辨识-->
<dubbo:application name="demotest-provider" owner="programmer" organization="dubbox"/>
<!--使用 zookeeper 注册中心暴露服务,注意要先开启 zookeeper-->
<dubbo:registry address="zookeeper://localhost:2181"/>
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!--使用 dubbo 协议实现定义好的 api.PermissionService 接口-->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" protocol="dubbo"/>
<!--具体实现该接口的 bean-->
<bean id="demoService" class="com.alibaba.dubbo.demo.impl.DemoServiceImpl"/>
</beans>
调用段
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="demotest-consumer" owner="programmer" organization="dubbox"/>
<!--向 zookeeper 订阅 provider 的地址,由 zookeeper 定时推送-->
<dubbo:registry address="zookeeper://localhost:2181"/>
<!--使用 dubbo 协议调用定义好的 api.PermissionService 接口-->
<dubbo:reference id="permissionService" interface="com.alibaba.dubbo.demo.DemoService" generic="true"/>
</beans>
// 方式1
public class Consumer {
public static void main(String[] args) {
/Spring泛化调用/
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("consumer.xml");
context.start();
System.out.println("consumer start");
GenericService demoService = (GenericService) context.getBean("permissionService");
System.out.println("consumer");
Object result = demoService.$invoke("getPermissions", new String[] { "java.lang.Long" }, new Object[]{ 1L });
System.out.println(result);
}
}
//方式2
public class Consumer {
public static void main(String[] args) {
// 普通编码配置方式
ApplicationConfig application = new ApplicationConfig();
application.setName("dubbo-consumer");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
reference.setApplication(application);
reference.setRegistry(registry);
reference.setInterface("com.alibaba.dubbo.demo.DemoService");
reference.setGeneric(true); // 声明为泛化接口
ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(reference);
// 基本类型以及Date,List,Map等不需要转换,直接调用
Object result = genericService.$invoke("getPermissions", new String[] { "java.lang.Long" }, new Object[] { 1L });
System.out.println(result);
}
}
三、挖穿实现原理【以Dubbo为例子】
1.原理图
+-------------------------------------------+ +-------------------------------------------+
| consumer 端 | | provider 端 |
| | | |
| | | |
| | | |
| | | |
| +------------------+ | | +--------------+ |
| |GenericImplFilter | | Invocation | |GenericFilter | |
| +----> | +-------------------------> | | |
| | +------------------+ | | +--------------+ |
| +-----------+ | | | +-----------+ |
| | | | | | | | |
| |Client | | | +--> | Service | |
| | | | | | | |
| +-----------+ | | +-------+---+ |
| | | | |
| ^ +------------------+ | | +--------------+ | |
| | |GenericImplFilter | | | |GenericFilter | <----------+ |
| +-------------+ | <-------------------------+ | |
| +------------------+ | | +--------------+ |
| | | |
| | | |
| | | |
| | | |
+-------------------------------------------+ +-------------------------------------------+
2.简化的原理
调用端
@Activate(group = CommonConstants.CONSUMER, value = GENERIC_KEY, order = 20000)
public class GenericImplFilter extends ListenableFilter {
private static final Logger logger = LoggerFactory.getLogger(GenericImplFilter.class);
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 ((invocation.getMethodName().equals($INVOKE) || invocation.getMethodName().equals($INVOKE_ASYNC))
&& invocation.getArguments() != null
&& invocation.getArguments().length == 3
&& ProtocolUtils.isGeneric(generic)) {
invocation.setAttachment(
GENERIC_KEY, invoker.getUrl().getParameter(GENERIC_KEY));
}
return invoker.invoke(invocation);
}
static class GenericImplListener implements Listener {
@Override
public void onResponse(Result appResponse, Invoker invoker, Invocation invocation) {
}
@Override
public void onError(Throwable t, Invoker invoker, Invocation invocation) {
}
}
}
服务端
@Activate(group = CommonConstants.PROVIDER, order = -20000)
public class GenericFilter extends ListenableFilter {
public GenericFilter() {
super.listener = new GenericListener();
}
@Override
public Result invoke(Invoker invoker, Invocation inv) throws RpcException {
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())) {
String name = ((String) inv.getArguments()[0]).trim();
String[] types = (String[]) inv.getArguments()[1];
Object[] args = (Object[]) inv.getArguments()[2];
try {
// 找到调用的接口
Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class[] params = method.getParameterTypes();
if (args == null) {
args = new Object[params.length];
}
String generic = inv.getAttachment(GENERIC_KEY);
if (StringUtils.isBlank(generic)) {
generic = RpcContext.getContext().getAttachment(GENERIC_KEY);
}
if (StringUtils.isEmpty(generic)
|| ProtocolUtils.isDefaultGenericSerialization(generic)) {
// 将PojoUtils 转换的简单对象 转换为复杂的对象
args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
}
return invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
} catch (NoSuchMethodException e) {
throw new RpcException(e.getMessage(), e);
} catch (ClassNotFoundException e) {
throw new RpcException(e.getMessage(), e);
}
}
return invoker.invoke(inv);
}
static class GenericListener implements Listener {
@Override
public void onResponse(Result appResponse, Invoker invoker, Invocation inv) {
if ((inv.getMethodName().equals($INVOKE) || inv.getMethodName().equals($INVOKE_ASYNC))
&& inv.getArguments() != null
&& inv.getArguments().length == 3
&& !GenericService.class.isAssignableFrom(invoker.getInterface())) {
String generic = inv.getAttachment(GENERIC_KEY);
if (StringUtils.isBlank(generic)) {
generic = RpcContext.getContext().getAttachment(GENERIC_KEY);
}
if (appResponse.hasException() && !(appResponse.getException() instanceof GenericException)) {
appResponse.setException(new GenericException(appResponse.getException()));
}
// 设置反序列化 讲复杂对象转换为简单的基础对象
appResponse.setValue(PojoUtils.generalize(appResponse.getValue()));
}
}
@Override
public void onError(Throwable t, Invoker invoker, Invocation invocation) {
}
}
}
3.原理总结
基于PojoUtils 简化复杂对象,不用引入二方包。
针对 com.alibaba.dubbo.rpc.service.GenericService.$invoke(String method, String[] parameterTypes, Object[] args) 这个接口进行特殊判断,基于拦截器处理特殊拦截。
基于拦截器,消费者端 GenericImplFilter 处理
1> 不考虑dubbo 其他的复杂序列化的需求很简单,基本上啥都不做
基于拦截器,服务提供者端 GenericFilter处理,
1> 基于接口、方法名称、方法参数类型查询具体的服务、服务的方法名称;
2> 基于PojoUtils 、方法参数类型、方法参数 反序列化为复杂的参数对象PojoUtils.realize(args, params, method.getGenericParameterTypes()) ;
3> 基于PojoUtils.generalize(appResponse.getValue()) 序列化返回值。
四、扬长揭短
泛化和非泛化的优缺点分析
1.优点
实现层面:服务消费者不需要有任何接口的实现,就能完成服务的调用,比如:只需要知道服务端的appkey 端口号 以及所暴漏thrfit服务的包名+类名就可以完成调用(无参数方法)。
编码层面:快速开发,降低开发成本,加快交付周期。
2.缺点
编码层面:参数传递复杂,不方便使用,比如:出参为json,结构不透明,强依赖提供方的api定义,目录结构,需要保持更新。
配置风险和代码变更风险比较高。
一些rpc相关辅助功能支持不友好,比如熔断,限流打点等。
3.其他
性能上剧测试报告显示无差别,多余耗时主要是序列和反序列化层面。
1.泛化调用1kByte和String性能差异不大
2.正常rpc调用和泛化调用的性能差异不大
正常rpc调用性能峰值约比泛化调用大1k左右,原因是泛化调用比正常调用多了一层json序列化,此过程占用性能比例不是很高。
从热点分析树上来看,两者热点方法基本相同,差异不是很大,都集中在服务端rpc底层读写和编解码阶段。
3.性能瓶颈:新生代大小,默认设置为4G
五、参考资料
记不得了,如果有侵权的地方请联系,我会及时删除,感谢。