本文记录微服务开发中遇到的一个问题。关于微服务可以搜索笔者的其他专栏文章。
将一个单体应用拆分成若干个微服务之后,不同微服务之间的接口请求与调用使用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
方法,发现之前另有一个业务功能模块里也在使用此方法。
这个功能之前是正常的吗?不知道!
抱着怀疑的态度,同时也是在验证。于是通过postman模拟请求此功能对应的controller层接口方法,是好的!!
那就把同步请求调用换成异步请求调用,即把上面截图里下面那个有问题的代码行,换成上面没有问题的形式:CompletableFuture.runAsync(() -> remotePaymentService.initialChannelPayGoodsList(channelId));
。
报错消失。
如果就此【算是解决】问题,以为大功告成。可能永远得不到长进吧。
报错是【消失】,可是为啥啊?
与此同时,继续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.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上。