Feign反序列化MismatchedInputException:Cannot deserialize instance of `Boolean` out of START_OBJECT token

概述

本文记录微服务开发中遇到的一个问题。关于微服务可以搜索笔者的其他专栏文章。

将一个单体应用拆分成若干个微服务之后,不同微服务之间的接口请求与调用使用Feign。一般而言,一个微服务对应于一个数据库,App1对应于Db1,APP2对应于Db2。如果某个业务需求下,想要同时查询或更新两个数据库,即Db1和Db2,则需要考虑选择一个服务提供远程接口,另一个服务来请求。至于选择哪个服务提供接口,则依据具体业务场景来定夺。

问题

本地开发时,debug模式启动2个应用:merchant 和 payment,应用启动时都注册到consul注册中心,merchant服务调用payment服务提供的接口。每个服务都有自己的单元测试类,在payment里通过单元测试事先验证payment服务的某个特定的接口方法可以通过单元测试,这个方法也就是merchant服务需要调用的远程接口。

然后,通过postman调试merchant服务的接口,意料之外地,postman接口请求失败,发现console控制台
打印的详细报错信息如下:

feign.codec.DecodeException: Error while extracting response for type [class java.lang.Boolean] and content type [application/json;charset=UTF-8];
nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.Boolean` out of START_OBJECT token;
nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.Boolean` out of START_OBJECT token
at [Source: (ByteArrayInputStream); line: 1, column: 1]
at com.aba.open.merchant.service.impl.MerchantAppServiceImpl.add(MerchantAppServiceImpl.java:109)

排查

遇到报错,第一反应是一脸懵逼,代码看起来没有问题呀。

第二反应,Google搜索报错信息,一时之间没有找到比较符合上面这个报错场景的相关文章。

于是只能继续看代码。上面也提到,在payment服务里,接口方法可以通过单元测试。问题的现象:merchant服务调用payment服务提供的方法,payment服务里可以看到打印日志,逻辑执行正常,数据库新增一条数据,接口响应正常。程序继续执行,回到merchant服务,然后出现报错。

也就是说,报错发生在服务调用之间(废话),事实上报错信息说得很明显,JSON反序列化失败。

至此,还是持续懵逼。

在merchant项目工程里全局搜索payment服务提供的initialChannelPayGoodsList方法,发现之前另有一个业务功能模块里也在使用此方法。
Feign反序列化MismatchedInputException:Cannot deserialize instance of `Boolean` out of START_OBJECT token_第1张图片
这个功能之前是正常的吗?不知道!

抱着怀疑的态度,同时也是在验证。于是通过postman模拟请求此功能对应的controller层接口方法,是好的!!

那就把同步请求调用换成异步请求调用,即把上面截图里下面那个有问题的代码行,换成上面没有问题的形式:CompletableFuture.runAsync(() -> remotePaymentService.initialChannelPayGoodsList(channelId));

报错消失。

深入

如果就此【算是解决】问题,以为大功告成。可能永远得不到长进吧。

报错是【消失】,可是为啥啊?

Feign

与此同时,继续Google搜索,发现一篇比较类似的文章FeignClient调用。

考虑到问题是出现在应用间的远程调用,此时才开始将关注点放在Feign上。

于是仔细查看Feign接口代码,payment服务提供的Feign接口如下:

@FeignClient(name = "payment-provider", fallbackFactory = RemotePaymentServiceFallbackFactory.class, configuration = FeignConfig.class)
public interface RemotePaymentService {
	/**
	 * 初始化渠道配置产品信息
	 *
	 * @param channel channel
	 */
	@RequestMapping(value = "/pay/initialChannelPayGoodsList", method = {RequestMethod.POST})
	Boolean initialChannelPayGoodsList(@RequestBody String channel);
}

payment服务里提供的controller接口方法如下:

@RestController
public class PayGoodsController {

	@ApiOperation(value = "初始化渠道配置产品信息", notes = "初始化查询渠道配置产品信息")
	@PostMapping(value = "/pay/initialChannelPayGoodsList")
	public Response<Boolean> initialChannelPayGoodsList(@RequestBody String channel) {
	    return Response.success(Boolean.TRUE);
	}
}

两个地方的方法定义返回值类型不一致。

改任何一个地方,与另外一个地方的接口定义保持一致,都可以解决问题。

推荐做法:将返回数据使用Response包装一下。

Response定义(有省略):

@Data
public class Response<T> implements Serializable {
	private static String SUCCESS = "success";
    private static String FAIL = "fail";
    private static final int SUCCESS_CODE = 0;
    private static final int ERROR_CODE = 9000;
    private int code;
    private String msg;
    private T data;
}

CompletableFuture

那,为何之前两个地方的接口方法定义不完全一致,即返回值类型不同时,使用CompletableFuture.runAsync(() -> remotePaymentService.initialChannelPayGoodsList(channelId));没有问题呢?

参考多线程与并发系列之CompletableFuture

CompletableFuture 方法执行过程若产生异常,当调用 get,join获取任务结果才会抛出异常。

也就是说,如果仅仅只是使用CompletableFuture的runAsync(),其他进程里被调用的方法(此处是remotePaymentService.initialChannelPayGoodsList(channelId))执行时产生的异常会被吞没,不会打印出报错日志。

事实上,CompletableFuture.runAsync(() -> remotePaymentService.initialChannelPayGoodsList(channelId));这一行代码,在执行完remotePaymentService.initialChannelPayGoodsList(channelId)后,payment服务里对应的数据库的数据发生变更,功能就完成了;merchant服务不打印错误日志,merchant服务其他代码片段提供的功能也实现了。

造成一切都很完美的假象。事实上却留下一个可大可小的bug隐患。

反思

文章写到这里,一路看下来,思路清晰无比。

事实上在问题排查过程中,并没有这么顺畅。一开始并没有将关注点放在Feign上。

参考

你可能感兴趣的:(Spring,Cloud,java,spring,cloud)