FeignClient调用 Cannot deserialize instance of `java.lang.Boolean` out of START_OBJECT token to

异常描述
Caused by: 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] 
异常原因
  1. 项目分为原子层(直连数据库)和聚合层(调用原子层) (DDD模式),原子上原子层不能互相调用,必须通过聚合层调。聚合层之间可以相互调用(抽取公共jar包)
  2. 原子层方法定义
# order项目
public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){
  ...
  String siOrders = "测试si单号";
  if(StringUtils.isNotBlank(siOrders)){
      throw new ServiceException("si单号:" + siOrders + "已经订舱");
  }
  ...
}

  1. 聚合层接口定义
public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){
	return ***Feign.saveTmsBookingOrder(vo);
}
  1. 当原子层抛出 ServiceException 异常时(其他异常类似)
  2. 通过postman直接调用原子层返回结果为
	{
	    "failed": true,
	    "code": "500",
	    "message": "si单号:WM000004504已经订舱",
	    "type": "warn",
	    "data": null
	}
  1. 聚合层正常返回数据(json串) 泛型封装在 body 属性中
{
  "body": true,
  "headers": {
    "cache-control": [
      "no-cache, no-store, max-age=0, must-revalidate"
    ],
    "connection": [
      "keep-alive"
    ],
    "content-type": [
      "application/json"
    ],
    "date": [
      "Wed, 08 Jun 2022 06:42:36 GMT"
    ],
    "expires": [
      "0"
    ],
    "pragma": [
      "no-cache"
    ],
    "transfer-encoding": [
      "chunked"
    ],
    "x-content-type-options": [
      "nosniff"
    ],
    "x-xss-protection": [
      "1; mode=block"
    ]
  },
  "statusCode": "OK",
  "statusCodeValue": 200
}
  1. 但在聚合层通过Feign调用原子层时,则返回开始时的异常
  2. 分析异常时,大概意思是返回值不能转换为 java.lang.Boolean
解法1(不建议)
  1. 原子层返回为
public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){
  ...
  String siOrders = "测试si单号";
  if(StringUtils.isNotBlank(siOrders)){
     // 正常情况不展示
  	 MultiValueMap<String, String> headers = new HttpHeaders();
     headers.set("errorMessage", URLEncoder.encode("si单号:****已经订舱", StandardCharsets.UTF_8.name()));
     ResponseEntity<Boolean> response = new ResponseEntity(false, headers, HttpStatus.OK);
     return response;
  }
  ...
}
  1. 将错误信息封装在Header中,当聚合层接收泛型返回值为false时,则从header头取错误描述(errorMessage)
  2. 统一返回参数封装

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

/**
 * Result
 * 统一返回参数封装
 *
 * @author weigang
 * @create 2022-06-09
 **/
@Getter
@Setter
@ToString
@NoArgsConstructor
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1848261014773540607L;

    /**
     * 是否成功
     */
    private boolean success;

    /**
     * 错误码
     */
    private String code;

    /**
     * 提示语
     */
    private String message;

    /**
     * 最好的方式是用泛型,明确返回类型
     */
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setSuccess(true);
        result.setCode("0");
        result.setData(data);
        result.setMessage("操作成功");

        return result;
    }

    public static <T> Result<T> error(String code, String message) {
        Result<T> result = new Result<>();
        result.setSuccess(false);
        result.setCode(code);
        result.setMessage(message);

        return result;
    }

}
  1. 聚合层定义修改为
public ResponseEntity<Result<Boolean>> saveTmsBookingOrder(@Valid SaveDTO vo){
    ResponseEntity<Boolean> response = ***Feign.saveTmsBookingOrder(vo);
    if (response.getBody()) {
        return Results.success(Result.success(true));
    }

    String errorMsg = "";
    List<String> errorList = response.getHeaders().get(CommonConstant.ERROR_MSG);
    if (CollectionUtils.isNotEmpty(errorList)) {
        try {
            errorMsg = URLDecoder.decode(errorList.get(0), StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            log.error("saveTmsBookingOrder fail, param-> {}, UnsupportedEncodingException-> {}", JSON.toJSONString(vo), e);
        }
    }
    return Results.success(Result.error("-1", errorMsg));
}
  1. Results 底层实现
	private static final ResponseEntity NO_CONTENT = new ResponseEntity<>(HttpStatus.NO_CONTENT);

    /**
     * 请求成功
     *
     * @param data 返回值
     * @return HttpStatus 200
     */
    public static <T> ResponseEntity<T> success(T data) {
        if (data == null) {
            return NO_CONTENT;
        }
        return ResponseEntity.ok(data);
    }
  1. 因为项目中原子层还有很多地方都是返回 ResponseEntity,其他接口也需要改造时,成本太高
解法2(不建议)
  1. 原子层和聚合层接口定义都修改为 ResponseEntity>,原子层封装好错误信后返回
  2. 每个接口都需要修改原子层和聚合层,上线稍不注意,就可能补录数据(返回参数变了)
  3. 针对少量使用 ResponseEntity> 接口,可以使用这种方式(且流量不大)
解法3
  1. 原子层接口定义不变
public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){
  ...
}
  1. 原子层增加Controller层拦截器
@Pointcut("execution(* com.tcl.***.controller.v1.*.*.*(..)) " +
            "|| execution(* com.tcl.***.controller.v1.*.*(..))")
   private void methodAspect() {
   }

   @Around("methodAspect()")
   public Object validate(ProceedingJoinPoint joinPoint) throws Throwable {
       Signature signature = joinPoint.getSignature();
       if (!(signature instanceof MethodSignature)) {
           return null;
       }

       MethodSignature methodSignature = (MethodSignature) signature;
       Method method = methodSignature.getMethod();
       String className = method.getDeclaringClass().getSimpleName();
       String methodName = method.getName();
       Object[] argArr = joinPoint.getArgs();

       Object argObj = null;

       if (!Objects.isNull(argArr) && argArr.length > 0) {
           Object[] arguments = new Object[argArr.length];
           for (int i = 0; i < argArr.length; i++) {
               if (argArr[i] instanceof MultipartFile) {
                   continue;
               }
               arguments[i] = argArr[i];
           }
           argObj = arguments[0];
       }

       String traceId = null;
       try {
           // REQUEST_ID 通过 logback-spring.xml 可以获取
           String traceIdParent = MDC.get(CommonConstant.REQUEST_ID);
           if (StringUtils.isBlank(traceIdParent)) {
               traceId = UUID.randomUUID().toString().replaceAll("-", "");
               MDC.put(CommonConstant.REQUEST_ID, traceId);
           } else {
               traceId = traceIdParent;
           }

           // multipartFile使用json.toJsonString会失败,驳回异常,不打印文件信息
           String argStr = JSON.toJSONString(argObj);
           log.info("traceId-> {}, {} {} param-> {}", traceId, className, methodName, argStr);
       } catch (Exception e) {
       }
       Object resultObj;
       Type typeIndex0 = this.getTypeIndex0(method);

       try {
           // 链路ID 通过 header 传到原子层(FeignInterceptor.java)
           resultObj = joinPoint.proceed();
           log.info("traceId-> {}, {} {} response-> {}", traceId, className, methodName, JSON.toJSONString(resultObj));
           return resultObj;
       } catch (IllegalArgumentException e) {
           log.warn("ControllerAspect throw IllegalArgumentException, traceId-> {}, invoke {} {} param-> {}; {}", traceId, className, methodName, JSON.toJSONString(argObj), e);
           // 不确定返回值类型,只能抛出异常
           throw e;
       } catch (ServiceException e) {
           if (Objects.nonNull(typeIndex0)) {
               // 可以在nacos上配置参数,防止代码异常,导致流程走不下去
               log.info("ControllerAspect throw ServiceException, actualTypeArguments-> {}", typeIndex0.getTypeName());
               MultiValueMap<String, String> headers = new HttpHeaders();
               headers.set("errorMessageTms", URLEncoder.encode(e.getMsg(), StandardCharsets.UTF_8.name()));
               if (Boolean.class == typeIndex0) {
                   ResponseEntity<Boolean> response = new ResponseEntity(false, headers, HttpStatus.OK);
                   return response;
               }
           }
           throw e;
       } catch (Exception e) {
           log.warn("ControllerAspect throw Exception, traceId-> {}, invoke {} {} param-> {}; {}", traceId, className, methodName, JSON.toJSONString(argObj), e);
           throw e;
       } catch (Throwable e) {
           log.warn("ControllerAspect throw Throwable, traceId-> {}, invoke {} {} param-> {}; {}", traceId, className, methodName, JSON.toJSONString(argObj), e);
           throw e;
       } finally {
           MDC.remove(CommonConstant.REQUEST_ID);
       }
   }

   private Type getTypeIndex0(Method method) {
       Class<?> returnType = method.getReturnType();
       if (returnType == ResponseEntity.class) {
           Type type = method.getGenericReturnType();
           if (type instanceof ParameterizedType) {
               Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
               return actualTypeArguments[0];
           }
       }

       return null;
   }
  1. catch 到 ServiceException 异常后进行处理(将解法1处理方式抽象出来)
  2. 聚合层定义(和解法1第四点完全相同)
public ResponseEntity<Result<Boolean>> saveTmsBookingOrder(@Valid SaveDTO vo){
    ResponseEntity<Boolean> response = ***Feign.saveTmsBookingOrder(vo);
    if (response.getBody()) {
        return Results.success(Result.success(true));
    }

    String errorMsg = "";
    List<String> errorList = response.getHeaders().get(CommonConstant.ERROR_MSG);
    if (CollectionUtils.isNotEmpty(errorList)) {
        try {
            errorMsg = URLDecoder.decode(errorList.get(0), StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            log.error("saveTmsBookingOrder fail, param-> {}, UnsupportedEncodingException-> {}", JSON.toJSONString(vo), e);
        }
    }
    return Results.success(Result.error("-1", errorMsg));
}
  1. 后续直接修改聚合层 Controller 即可
  2. 或者在聚合层 Controller 层的拦截器中,指定方法名为配置方法时,将 ResponseEntity 转换为 ResponseEntity>,则只需稍微修改聚合层 Controller 即可
// 伪代码
	@Value("${response.boolean2result.method:method1,method2}")
    private String resultMethod;

	List<String> resultMethodList = Arrays.as(StringUtils.split(resultMethod, ","));
	ResponseEntity<Boolean> response = new ResponseEntity(false, HttpStatus.OK);
	if(resultMethodList.contains("method1") {
	    // 这里肯定转不成功,需从header头取出信息,封装到response中
		response = (ResponseEntity<Result<Boolean>>)response;
	}
疑问❓
  1. 原子层和聚合层接口定义都修改为 ResponseEntity>
  2. 原子层使用最初代码,直接抛异常
  throw new ServiceException("si单号:" + siOrders + "已经订舱");
  1. 聚合层能正常接收吗(没测试)

你可能感兴趣的:(Spring,Cloud,Spring,Boot,知识总结,java,FeignClient,反序列化)