异常描述
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 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
}
- 但在聚合层通过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;
@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;
}
}
- 聚合层定义修改为
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);
public static <T> ResponseEntity<T> success(T data) {
if (data == null) {
return NO_CONTENT;
}
return ResponseEntity.ok(data);
}
- 因为项目中原子层还有很多地方都是返回 ResponseEntity,其他接口也需要改造时,成本太高
解法2(不建议)
- 原子层和聚合层接口定义都修改为 ResponseEntity>,原子层封装好错误信后返回
- 每个接口都需要修改原子层和聚合层,上线稍不注意,就可能补录数据(返回参数变了)
- 针对少量使用 ResponseEntity> 接口,可以使用这种方式(且流量不大)
解法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 {
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;
}
String argStr = JSON.toJSONString(argObj);
log.info("traceId-> {}, {} {} param-> {}", traceId, className, methodName, argStr);
} catch (Exception e) {
}
Object resultObj;
Type typeIndex0 = this.getTypeIndex0(method);
try {
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)) {
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 转换为 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") {
response = (ResponseEntity<Result<Boolean>>)response;
}
疑问❓
- 原子层和聚合层接口定义都修改为 ResponseEntity>
- 原子层使用最初代码,直接抛异常
throw new ServiceException("si单号:" + siOrders + "已经订舱");
- 聚合层能正常接收吗(没测试)