异常描述
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]
异常原因
项目分为原子层(直连数据库)和聚合层(调用原子层) (DDD模式),原子上原子层不能互相调用,必须通过聚合层调。聚合层之间可以相互调用(抽取公共jar包)原子层方法定义# order项目public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){...String siOrders = "测试si单号";if(StringUtils.isNotBlank(siOrders)){throw new ServiceException("si单号:" + siOrders + "已经订舱");}...}
聚合层接口定义
public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){return ***Feign.saveTmsBookingOrder(vo);}
当原子层抛出 ServiceException 异常时(其他异常类似)通过postman直接调用原子层返回结果为
{"failed": true,"code": "500","message": "si单号:WM000004504已经订舱","type": "warn","data": null}
聚合层正常返回数据(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 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}
但在聚合层通过Feign调用原子层时,则返回开始时的异常分析异常时,大概意思是返回值不能转换为 java.lang.Boolean
解法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;}...}
将错误信息封装在Header中,当聚合层接收泛型返回值为false时,则从header头取错误描述(errorMessage)统一返回参数封装
import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter;import lombok.ToString;import java.io.Serializable;/*** Result* 统一返回参数封装** @author weigang* @create -06-09**/@Getter@Setter@ToString@NoArgsConstructorpublic 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;}}
聚合层定义修改为
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));}
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);}
因为项目中原子层还有很多地方都是返回 ResponseEntity<Boolean>,其他接口也需要改造时,成本太高
解法2(不建议)
原子层和聚合层接口定义都修改为 ResponseEntity<Result<Boolean>>,原子层封装好错误信后返回每个接口都需要修改原子层和聚合层,上线稍不注意,就可能补录数据(返回参数变了)针对少量使用 ResponseEntity<Result<Boolean>> 接口,可以使用这种方式(且流量不大)解法3
原子层接口定义不变public ResponseEntity<Boolean> saveTmsBookingOrder(@Valid SaveDTO vo){...}
原子层增加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;}
catch 到 ServiceException 异常后进行处理(将解法1处理方式抽象出来)聚合层定义(和解法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));}
后续直接修改聚合层 Controller 即可或者在聚合层 Controller 层的拦截器中,指定方法名为配置方法时,将 ResponseEntity<Boolean> 转换为 ResponseEntity<Result<Boolean>>,则只需稍微修改聚合层 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;}
疑问❓
原子层和聚合层接口定义都修改为 ResponseEntity<Result<Boolean>>原子层使用最初代码,直接抛异常throw new ServiceException("si单号:" + siOrders + "已经订舱");
聚合层能正常接收吗(没测试)