关于OpenFeign那点事儿 - 使用篇( 二 )


关于OpenFeign那点事儿 - 使用篇

文章插图
 
这里就可以详细看到,在请求开始的时候携带的所有header信息以及请求参数信息,在响应回来的时候通用打印了所有的响应信息 。
场景二:透传header信息在上一节中,我们在日志中看到了很多的请求头header的信息,这些都是程序自己添加的吗 ? 很明显不是 。例如x-application-name和x-request-id,这两个参数就是我们自己添加的 。
需要透传header信息的场景,一般是出现在租户ID或者请求ID的场景下 。我们这里以请求ID为例,我们知道用户的一个请求,可能会涉及多个服务实例,当程序出现问题的时候为了方便排查,我们一般会使用请求ID来标识一次用户请求,并且这个请求ID贯穿所有经过的服务实例,并且在日志中打印出来 。这样子,当出现问题的时候,根据该请求ID就可以捞出本次用户请求的所有日志信息 。
关于请求ID打印到日志可以参考:
不会吧,你还不会用RequestId看日志 ?[1]
基于这种场景,我们需要手动设置透传信息,Feign组件也给我们提供了对应的方式 。只要实现了RequestInterceptor接口,即可透传header信息 。
public class FeignRequestInterceptor implements RequestInterceptor {@Value("${spring.application.name}")private String app;@Overridepublic void apply(RequestTemplate template) {HttpServletRequest request = WebContext.getRequest();// job 类型的任务,可能没有Requestif(request != null && request.getHeaderNames() != null){Enumeration<String> headerNames = request.getHeaderNames();while (headerNames.hasMoreElements()) {String name = headerNames.nextElement();// Accept值不传递,避免出现需要响应xml的情况if ("Accept".equalsIgnoreCase(name)) {continue;}String values = request.getHeader(name);template.header(name, values);}}template.header(CommonConstants.APPLICATION_NAME, app);template.header(CommonConstants.REQUEST_ID, RequestIdUtil.getRequestId().toString());template.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);}}场景三:异常处理在第一节我们客户调用的时候,我们是没有处理异常的,我们直接.getData()直接返回了,这其实是一个非常危险的操作,.getData()的返回结果可能是null,很容易造成NPE的情况 。
SysUserResp sysUser = sysUserFeignApi.infoByMobile(mobile).getData();回想下,当我们调用其他当前服务的Service方法的时候,如果遇到异常,是不是就是直接抛出异常,交由统一异常处理器进行处理?所以,这里我们也是期望调用Feign和Service一样,遇到异常,交由统一异常进行处理 。
如何处理这个需求呢? 我们可以在解码的时候进行处理 。
@Slf4jpublic class FeignDecoder implements Decoder {// 代理默认的解码器private Decoder decoder;public FeignDecoder(Decoder decoder) {this.decoder = decoder;}@Overridepublic Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {// 序列化为jsonString json = this.getResponseJson(response);this.processCommonException(json);return decoder.decode(response, type);}// 处理公共业务异常private void processCommonException(String json){if(!StringUtils.hasLength(json)){return;}ApiResponse resp = JSONUtil.toBean(json, ApiResponse.class);if(resp.getSuccess()){return;}log.info("feign response error: code={}, message={}", resp.getCode(), resp.getMessage());// 抛出我们期望的业务异常throw new CommonException(resp.getCode(), resp.getMessage());}// 响应值转json字符串private String getResponseJson(Response response) throws IOException {try (InputStream inputStream = response.body().asInputStream()) {return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);}}}这里我们的处理方式是在解码的时候,先从响应结果中拿到是否有业务异常的判断,如果有,则构造业务异常实例,然后抛出信息 。
运行下代码,我们会发现统一异常那边还是无法处理由下游服务返回的异常,原因是虽然我们抛出了一个CommonException,但是其实最后还是会被Feign捕获,然后重新封装为DecodeException异常,再进行抛出
Object decode(Response response, Type type) throws IOException {try {return decoder.decode(response, type);} catch (final FeignException e) {throw e;} catch (final RuntimeException e) {// 重新封装异常throw new DecodeException(response.status(), e.getMessage(), response.request(), e);}}所以,我们还需要在统一异常那边再做下处理,代码如下:
@ResponseStatus(HttpStatus.OK)@ExceptionHandler(DecodeException.class)public ApiResponse decodeException(DecodeException ex){log.error("解码失败: {}", ex.getMessage());String id = RequestIdUtil.getRequestId().toString();if(ex.getCause() instanceof CommonException){CommonException ce = (CommonException)ex.getCause();return ApiResponse.error(id, ce.getErrorCode(), ce.getErrorMessage());}return ApiResponse.error(id, CommonExCodeEnum.DATA_PARSE_ERROR.getCode(), ex.getMessage());}


推荐阅读