什么是泛化调用
泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现。
官方文档
说白了,消费者可以在没有Interface接口的情况下去调用远程服务,由于没有接口和模型类元,消费者必须手动指定要调用的接口名、方法名、参数列表、版本号等信息。
正是因为没有接口和模型类元,所以泛化调用的接口返回结果Dubbo不得不转换成HashMap返回,由消费者自己去组装数据。
Dubbo官方给出的使用场景是框架测试集成,但是我们公司想借用这种特性,去掉传统的Controller层,由一个入口ApiController来完成所有Service层的调用。
统一入口
@RestController
public class ApiController {
/**
* 接口调用统一入口
* @param dubboRequest
* @return
*/
@RequestMapping("/api")
public Object api(@RequestBody DubboRequest dubboRequest) {
return DubboProxy.invoke(dubboRequest);
}
}
DubboRequest封装,你要调用哪个服务?
@Data
public class DubboRequest {
private String interfaceName;//接口名
private String methodName;//方法名
private String[] argTypes;//参数类型列表
private Object[] parameters;//参数
private String version;//版本号
}
根据DubboRequest泛化调用接口
public class DubboProxy {
// ReferenceConfig实例很重,缓存
private static ConcurrentMap<String, ReferenceConfig<GenericService>> CACHE = new ConcurrentHashMap<>();
public static Object invoke(DubboRequest dubboRequest) {
GenericService service = getService(dubboRequest);
if (service == null) {
// TODO 返回错误提示
return null;
}
return service.$invoke(dubboRequest.getMethodName(), dubboRequest.getArgTypes(), dubboRequest.getParameters());
}
private static GenericService getService(DubboRequest dubboRequest){
String key = dubboRequest.getInterfaceName() + dubboRequest.getVersion();
ReferenceConfig<GenericService> reference = CACHE.get(key);
if (reference != null) {
return reference.get();
}
synchronized (CACHE) {
if (CACHE.get(key) == null) { // recheck
reference = new ReferenceConfig<GenericService>();
// 弱类型接口名
reference.setInterface(dubboRequest.getInterfaceName());
reference.setVersion(dubboRequest.getVersion());
// 声明为泛化接口
reference.setGeneric(true);
reference.setTimeout(10000);//超时
reference.setRetries(0);//重试次数
CACHE.put(key, reference);
}
}
return ReferenceConfigCache.getCache().get(reference);
}
}
返回值问题
基于这种架构,去掉了Controller层,减少了代码量,但是使用过程中发现了一个新的问题:返回值问题。
泛化接口调用方式主要用于客户端没有 API 接口及模型类元的情况,参数及返回值中的所有 POJO 均用 Map 表示
消费者由于没有接口定义,导致泛化调用返回的结果全部转成HashMap,这样就导致返回的数据并不是前端想要的结果。例如:LocalDateTime
。
例如Service层返回如下类实例
public class User implements Serializable {
private static final long serialVersionUID = 7886320169374810190L;
private Long id;
private String name;
private LocalDateTime createTime;
}
调用接口,前端拿到的数据却是这样的:
面对这种情况如何解决呢?返回结果是Dubbo决定的,我们无法人为干预。网上查找资料,也没有查到结果,于是决定自己Debug跟踪Dubbo源码,最终找到解决方案,特此记录。
返回值问题解决
实现类返回的是User实例,Dubbo在将结果通过网络IO发送给消费者前肯定做了一层转换,将User转Map。
只要能找到转换的代码在哪里,看看是否可以人为干预一下,理论上就可以解决问题了。
笔者一路Debug,过程就不祥叙了,最终发现转换是在org.apache.dubbo.rpc.filter.GenericFilter
里通过PojoUtils.generalize()
方法实现的。
感兴趣的同学可以去看下Dubbo源码PojoUtils
类,这里就不贴了。PojoUtils只对一些常用类型做了特殊处理,如:Collection、Map等,像LocalDateTime是没做处理的,直接反射获取属性转Map了。
那么如何重写序列化逻辑呢?
Dubbo的源码我们又不能去修改它,我们可以选择将Dubbo源码拉下来,然后修改PojoUtils类,重新打包作为公司内部版本用,但是这样不利于后面的升级。
最终找到了解决方案:禁用原生GenericFilter,自定义GenericFilter。
重写GenericFilter
1、禁用原生GenericFilter
dubbo:
provider:
filter: -generic #禁用原生的GenericFilter
2、自定义返回值序列化逻辑
/**
* @Author: pch
* @Date: 2020/9/23 17:09
* @Description: Dubbo RPC调用数据传输时的序列化器
*/
public interface RpcSerializer<T> {
// 将对象转换成你想要传输的格式
Object serialize(T t);
}
/**
* @Author: pch
* @Date: 2020/9/23 17:39
* @Description: LocalDateTime的序列化方式
*/
@Component
public class LocalDateTimeSerializer implements RpcSerializer<LocalDateTime> {
private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public Object serialize(LocalDateTime localDateTime) {
return localDateTime.format(dateTimeFormatter);
}
}
public class RpcSerializerHolder {
private static final Map<Class, RpcSerializer> SERIALIZER_MAP = new ConcurrentHashMap<>();
static {
Collection<RpcSerializer> serializers = SpringUtil.getBeansOfType(RpcSerializer.class);
for (RpcSerializer serializer : serializers) {
for (Method method : serializer.getClass().getMethods()) {
if (!"serialize".equals(method.getName())) {
continue;
}
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length <= 0) {
continue;
}
Class<?> parameterType = parameterTypes[0];
if (Object.class.equals(parameterType)) {
continue;
}
SERIALIZER_MAP.put(parameterType, serializer);
}
}
}
public static RpcSerializer getByType(Class c){
return SERIALIZER_MAP.get(c);
}
}
3、自定义GenericFilter
模仿GenericFilter写就行了,为了不影响其他功能,建议不要动其他代码,只要将PojoUtils换成我们自定义的就可以了,篇幅原因,这里就不贴代码了。
4、修改PojoUtils.generalize()序列化逻辑
多加如下几行代码,如果Class属于自定义类型,就按照自定义的逻辑去转换它,而不是Dubbo默认的Map方式。
// 自定义的序列化类型
RpcSerializer serializer = RpcSerializerHolder.getByType(pojo.getClass());
if (serializer != null) {
return serializer.serialize(pojo);
}
5、启动服务,重新测试