Dubbo泛化调用强制返回Map问题,实现自定义返回值序列化逻辑

什么是泛化调用

泛化接口调用方式主要用于客户端没有 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、启动服务,重新测试
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值