背景
在项目开发中,如果我们没有对参数做校验或对业务处理做判断,经常会将一串让人头大的错误信息抛给前端,用户体验直接下降到零,而且也会将一些关键信息暴露出去,造成不可估量的错误。
现在大多数项目都采用了微服务架构,前后端分离开发,项目复杂度高,前后端职责划分很清楚,如果没有响应数据随便定义,前端人员会疯掉的,而且也不利于我们后期排查错误。
现在大多数项目都用的是Spring框架,接下来我们将利用Spring框架来解决以上两个问题。
响应格式
@Data@ToStringpublic class Result implements Serializable { private int code; private String msg; private T bean; public static Result success() { return success(null); } public static Result success(T bean) { Result result = new Result<>(); result.code = ResultType.SUCCESS.getCode(); result.msg = ResultType.SUCCESS.getMsg(); result.bean = bean; return result; } public static Result fail(String msg) { return fail(ResultType.FAIL.getCode(), msg); } public static Result fail(String msg, T bean) { return fail(ResultType.FAIL.getCode(), msg, bean); } public static Result fail(ResultType resultCode) { return fail(resultCode.getCode(),resultCode.getMsg()); } public static Result fail(ResultType resultCode, T bean) { return fail(resultCode.getCode(), resultCode.getMsg(), bean); } public static Result fail(int code, String msg) { Result result = new Result<>(); result.code = code; result.msg = msg; return result; } public static Result fail(int code, String msg, T bean) { Result result = new Result<>(); result.code = code; result.msg = msg; result.bean = bean; return result; }}
开发过程中我们约定后端返给前端响应格式统一为JSON,如下所示:
{ "code": 10000, "msg": "操作成功", "bean": null}
在响应参数中,主要包括三个内容:
- code:结果状态码,
- msg:响应信息
- bean:具体信息
结果状态码
@Getter@AllArgsConstructorpublic enum ResultType { PARAM_IS_INVALID(11101,"参数无效"), FORBIDDEN(11102,"没有权限,请联系管理员"), UNAUTHORIZED(11103,"认证失败,请重新认证"), CLIENT_DETAILS_UNAUTHORIZED(11104,"客户端资源校验失败"), FAIL(11105,"请检查 API 是否异常"), /*成功状态码10000*/ SUCCESS(10000,"操作成功"); private int code; private String msg;}
结果状态码可以让我们快速定位错误和区分错误,如果随便定义我们很难区分该状态码对应的信息,正确姿态应该是将状态码分组。比如:
结果状态码为4位;10001-19999表示通用错误;20001-29999表示业务处理错误。
这样当我们获取到20002的结果状态码,马上就知道是业务处理错误。
在微服务架构中,有时需要根据响应结果定位到哪个服务时,当我们遵循上面定义,前端获取到结果状态码后,我们会发现根本不能定位具体是哪个服务抛出的异常。
在复杂的微服务架构中,一般我们会这样定义响应参数:
结果状态码为5位,前两位表示具体服务,后三位表示具体错误码;11 -> 通用;12 -> 订单;;111001-11199表示通用错误;122001-12299表示业务处理错误;
这样当我们获取到122001就可以马上定位到是哪个服务出现的什么错误。
统一异常处理
在Spring中可以利用@ControllerAdvice实现统一异常处理
@Slf4j@RestControllerAdvicepublic class BaseExceptionHandlerAdvice extends ResponseEntityExceptionHandler { /** * 剩余异常不能捕获的错误,统一返回500 * * @param throwable * @return */ @ExceptionHandler(value = Throwable.class) public ResponseEntity handlerThrowable(Throwable e) { log.error("异常错误:{}" ,e.getMessage(), e); Result result = Result.fail(ResultType.FAIL); return WebUtil.outEntity(result); } /** * 业务异常 * * @param baseException * @return */ @ExceptionHandler(value = {BaseException.class}) public ResponseEntity handlerBaseException(BaseException e) { log.error("异常错误:{}" ,e.getMessage(), e); Result result = Result.fail(e.getMessage()); return WebUtil.outEntity(result); } /** * springmvc内部异常 * @param e * @param body * @param headers * @param status * @param request * @return */ @Override protected ResponseEntity handleExceptionInternal(Exception e, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if(e instanceof BindException || e instanceof MethodArgumentNotValidException) { log.error("异常类型{},数据效验出现问题{}",e.getClass(),e.getMessage()); BindingResult bindingResult = null; if(e instanceof BindException) { bindingResult = ((BindException)e).getBindingResult(); } else { bindingResult = ((MethodArgumentNotValidException)e).getBindingResult(); } Map errMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError) -> { errMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); return ResponseEntity.ok(Result.fail(ResultType.PARAM_IS_INVALID,errMap)); } else { log.error("异常错误:{}" ,e.getMessage(), e); if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { request.setAttribute("javax.servlet.error.exception", e, 0); } return ResponseEntity.ok(Result.fail(ResultType.FAIL)); } }}
BaseExceptionHandlerAdvice类中把异常分为三类
- Spring内部抛出的异常
- 自定义异常:BaseException
- 未知异常:Throwable
Spring内部抛出的异常
BaseExceptionHandlerAdvice继承ResponseEntityExceptionHandler,ResponseEntityExceptionHandler中封装了各种SpringMVC在处理请求时可能抛出的异常的处理。
public abstract class ResponseEntityExceptionHandler { @ExceptionHandler(value={ NoSuchRequestHandlingMethodException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, MethodArgumentNotValidException.class, MissingServletRequestPartException.class, BindException.class, NoHandlerFoundException.class }) public final ResponseEntity handleException(Exception ex, WebRequest request) throws Exception { ...省略 }}
在SpringMVC处理请求抛出的类中,我们主要处理BindException和MethodArgumentNotValidException异常,分别是处理form表单和json请求参数校验失败异常。统一返回格式为:
{ "code": 11101, "msg": "参数无效", "bean": { "name": "学生姓名不能为空" }}
其他异常统一返回
{ "code": 11105, "msg": "请检查 API 是否异常", "bean": null}
404异常
上面我们讲到可以利用Spring中的@ControllerAdvice实现统一异常处理。但是当我们发送一个项目中不存在的地址,发现统一异常处理机制并没有帮我们拦截到,会在控制台打印如下格式的响应:
{ "timestamp": "2020-10-14T02:59:48.427+00:00", "status": 404, "error": "Not Found", "message": "", "path": "/hello/bind31"}
在Spring Boot中提供了一个映射,该映射以合理的方式处理所有错误,我们只需要重写ErrorAttributes就可以将404页返回我们想要的格式,代码如下:
@Componentpublic class BaseErrorAttributes extends DefaultErrorAttributes { private ObjectMapper objectMapper = new ObjectMapper(); @Override public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map result = null; try { result = objectMapper.readValue(objectMapper.writeValueAsString(Result.fail(ResultType.FAIL)), HashMap.class); } catch (JsonProcessingException e) { e.printStackTrace(); } return result == null ? MapUtil.empty() : result; }}
ResponseBodyAdvice包装通用返回类型
在项目开发过程中,我们为了返回统一响应格式,必须按照规范在Controller方法返回约定的类,然而在有些情况下我们会忘记直接返回业务对象,为了避免造成这样的问题,我们可以利用Spring为我们提供的ResponseBodyAdvice接口,统一包装返回值。
@ControllerAdvice(basePackages = "com.gz.spring.result.controller")public class BaseRequestBodyAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { return !returnType.getMethod().getReturnType().isAssignableFrom(Void.TYPE) && converterType.isAssignableFrom(MappingJackson2HttpMessageConverter.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if(!(body instanceof Result)) { return Result.success(body); } return body; }}
具体代码结构