异常描述
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<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 {
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<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" ) {
response = ( ResponseEntity < Result < Boolean > > ) response;
}
疑问❓
原子层和聚合层接口定义都修改为 ResponseEntity<Result<Boolean>> 原子层使用最初代码,直接抛异常
throw new ServiceException ( "si单号:" + siOrders + "已经订舱" ) ;
聚合层能正常接收吗(没测试)