SpringBoot全局异常统一处理、反参标准化
对于日常的开发过程中出现的异常,我把它分为两种,
一种是需要给前端返回的异常,这种异常通常有入参格式、字段缺少、以及相关的业务异常,需要明确的告诉前端出现了什么问题,前端才好处理,
而另一种异常例如空指针、连接超时、io异常,这类型的异常不需要前端知晓,统一返回服务器异常即可。
所以我们需要捕获异常,对异常进行分类,然后再将封装成固定的格式返回给前端。
首先第一步个自定义一个ExceptionMap,这里其实啥也没实现就是改了个名字,这样在代码的可读性上能增加不少(我觉得)。
其实不做这一步直接用HashMap也行。我们都知道异常的抛出是冒泡的形式抛出的,现在要做的就是捕获,获取异常的内容,ExceptionMap就是用来异常被捕获后将异常的信息转化成一个map,后续再进行格式化和返回
/** * 自定义HashMap的ExceptionMap controller抛出的异常被处理成ExceptionMap * @author xuwang * @date 2019年5月29日 15:04:49 */ public class XcCuisineExceptionMap extends HashMap { }
第二步需要继承Exception实现一个新的业务Exception类,
这样做有两个用处,一个是可以自定义异常的内容,一个是可以和其他异常区分出来
/** * @ClassName: XcCuisineBusinessException * @ClassNameExplain: * @Description: * @author xuwang * @date 2019年05月31日 10:34:00 * */ public class XcCuisineBusinessException extends Exception { private static final long serialVersionUID = 1; private int code; private String errorMsg; public XcCuisineBusinessException(int code, String errorMsg) { super(errorMsg); this.code = code; this.errorMsg = errorMsg; } public XcCuisineBusinessException(int code, String errorMsg, Throwable throwable) { super(errorMsg, throwable); this.code = code; this.errorMsg = errorMsg; } public int getCode() { return code; } public String getErrorMsg() { return errorMsg; } }
第三步实现一个ControllerAdvice,这一步的目的主要就是捕获全局异常,所有从Controller抛出的异常都能在这捕获到,
在ExceptionHandler中,根据getClass().getName()区分出业务异常,和服务器异常,将异常变成一个拥有code和message的XcCuisineExceptionMap
这里还额外判断了一个MethodArgumentNotValidException ,MethodArgumentNotValidException是使用@Valid对入参里字段进行限制后,字段不符合规则出现的异常,这种也属于业务异常,但是没法抛出变成XcCuisineBusinessException,就在这判断了一下。
/** * @ClassName: XcCuisineControllerAdvice * @ClassNameExplain: * @Description: * @author xuwang * @date 2019年05月31日 10:34:00 * */ @ControllerAdvice public class XcCuisineControllerAdvice { static final Logger logger = LoggerFactory.getLogger(XcCuisineControllerAdvice.class); /** * 全局异常捕捉处理 * @param ex * @return */ @ResponseBody @ExceptionHandler(value = Exception.class) public Map errorHandler(Exception ex) { Map map = new XcCuisineExceptionMap(); if(ex.getClass().getName().equals(XcCuisineBusinessException.class.getName())){ XcCuisineBusinessException bex = (XcCuisineBusinessException) ex; map.put("code", bex.getCode()); map.put("msg", bex.getErrorMsg()); }else if(ex.getClass().getName().equals(MethodArgumentNotValidException.class.getName())){ MethodArgumentNotValidException mex = (MethodArgumentNotValidException) ex; StringBuffer sb = new StringBuffer(); List<FieldError> errorList = mex.getBindingResult().getFieldErrors(); for (FieldError error : errorList) { sb.append(error.getObjectName()); sb.append("对象的"); sb.append(error.getField()); sb.append("字段"); sb.append(translationString(error.getDefaultMessage())); } map.put("code",ExceptionConstants.PARAM_INVALID_CODE); map.put("msg", sb.toString()); }else { map.put("code", ExceptionConstants.SERVER_EXCEPTION_CODE); map.put("msg", ExceptionConstants.SERVER_EXCEPTION_MSG); } return map; } /** * @Title: translationString * @TitleExplain: * @Description: 对字符串进行转译 解决报错中出现json关键字符 导致json序列化失败的问题 * @param * @return java.lang.String * @version * @author */ private String translationString(String string){ String temp = GsonUtil.toJson(string); return temp.substring(1, temp.length()-1); } }
第四步是在创建转化器,我的这个转换器其实对入参和反参都进行了转换,因为在我的项目里入参也是有标准的,不过这里没写太多,
在writeInternal中,反参如果是XcCuisineExceptionMap,直接就转换成JSON字符串返回,这里还在正常返回的内容中添加了code和message,做到了反参的标准化{code:"",msg:"",data:{}}
/** * @author xuwang * @ClassName: GsonHttpMessageConverter * @ClassNameExplain: Gson转换器 * @Description: * @date 2019年05月30日 20:13:04 */ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter { private final static String CODE_KEY = "code"; private final static String MSG_KEY = "msg"; private final static String DATA_KEY = "data"; private final static String CONTENT_TYPE_KEY = "Content-Type"; public GsonHttpMessageConverter() { super(new MediaType("application", "json", Charset.forName("utf-8"))); } @Override protected boolean supports(Class aClass) { //返回true表示支持所有的类 return true; } /** * 处理请求内容 * @param aClass * @param httpInputMessage * @return * @throws IOException * @throws HttpMessageNotReadableException */ @Override protected Object readInternal(Class aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException { //取出requestBody中内容 String json = StreamUtils.copyToString( httpInputMessage.getBody(), httpInputMessage.getHeaders().getContentType().getCharset()); //打印入参 logger.debug(aClass+" request json : \n" + json); if(StringUtils.isNotEmpty(json) && json.trim().startsWith("{") && json.trim().endsWith("}")){ return GsonUtil.json2Bean(json, aClass); }else{ //TODO throw new RequestFormatException("请求格式不正确"); return json; } } /** * 处理响应内容 * @param o * @param httpOutputMessage * @throws IOException * @throws HttpMessageNotWritableException */ @Override protected void writeInternal(Object o, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException { Class oClass = o.getClass(); String json = ""; if(oClass.getName().equals(XcCuisineExceptionMap.class.getName())){ //判断如果是抛出来的异常直接转换为json字符串 json = GsonUtil.toJson(o); }else { Map<String, Object> result = new LinkedHashMap<>(); //设置code result.put(CODE_KEY, Constant.CORRECT_CODE); //设置msg result.put(MSG_KEY, Constant.CORRECT_MSG); //设置data result.put(DATA_KEY, o == null ? "" : o); json = GsonUtil.toJson(result); }; logger.debug("response json : \n" + json); httpOutputMessage.getHeaders().add(CONTENT_TYPE_KEY, "application/json"); httpOutputMessage.getBody().write(json.getBytes("UTF-8")); } }
最后是注册这个转换器
@EnableWebMvc @Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void configureMessageConverters( List<HttpMessageConverter<?>> converters) { //清理其他转换器,添加自定义转换器 converters.clear(); converters.add(createGsonHttpMessageConverter()); } @Bean public GsonHttpMessageConverter createGsonHttpMessageConverter() { //注入自定义转换器 return new GsonHttpMessageConverter(); } }
到此就完成了。