统一返回格式 为什么需要统一返回格式 接口的返回值类型众多,有的直接返回数据传输对象(DTO),甚至直接返回数据对象(DO),还有的返回Result对象。这样对外交互时对方处理起来特别的复杂,所以需要统一的返回格式。
构建统一返回格式 构建Result对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class ResultHelper { // 成功的响应结果 public static <T> Result<T> success(T data) { return new Result<T>() .setCode(Result.SUCCESS_CODE) .setData(data) .setMessage(Result.SUCCESS_MESSAGE) .setTimestamp(System.currentTimeMillis()); } // 失败的响应结果 public static <T> Result<T> failure(String code, String message) { return new Result<T>() .setCode(code) .setMessage(message) .setTimestamp(System.currentTimeMillis()); } // 失败的响应结果,使用默认的错误码 public static <T> Result<T> failure(String message) { return new Result<T>() .setCode(ErrorCode.SERVICE_ERROR.getCode()) .setMessage(message) .setTimestamp(System.currentTimeMillis()); } public static <T> Result<T> failure(ErrorCode errorCode) { return new Result<T>() .setCode(errorCode.getCode()) .setMessage(errorCode.getMessage()) .setTimestamp(System.currentTimeMillis()); } }
访问下接口
1 2 3 4 5 6 7 8 @RestController @RequestMapping("/user") public class UserController { @PostMapping("/test") public Result<String> test() { return ResultHelper.success("test"); } }
结果为:
统一的模板方法 为什么需要统一的模板方法 按照上面的示例一切都很美好,代码也很简洁,内容也如预期做了输出,但是我们考虑一个问题,如果有上百个接口的时候,需要对接口进行一些公共代码的调整,比如讲参数和输出打印出来,对参数进行验证,统计代码执行的时间。当遇到这种需求可以使用AOP做切面编程,但是切面编程的效率并不是很好,所以可以选择使用统一的模板方法来处理这些问题。既可以实现统一的管理公共代码,又可以更规范的书写代码
构建统一的模板方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Slf4j public abstract class ServerTemplate<T, R> { public R Process(T request) { log.info("开始执行,参数:{}", request); StopWatch stopWatch = new StopWatch(); stopWatch.start(); try { // 参数校验 validParam(request); // 执行业务代码 R response = doProcess(request); // 记录时间信息 stopWatch.stop(); log.info("执行结束,耗时:{}ms", stopWatch.getTotalTimeMillis()); return response; } catch (Exception e) { log.error("执行异常,异常信息:{}", Arrays.toString(e.getStackTrace())); // 抛出异常 统一处理 throw e; } } protected abstract void validParam(T request); protected abstract R doProcess(T request);
使用统一格式创建一个接口 改造一下之前的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @PostMapping("/test") public Result<String> test(@RequestBody String request) { return ResultHelper.success((new ServerTemplate<String, String>(){ @Override protected void validParam(String request) { } @Override protected String doProcess(String request) { return request; } }).process(request)); }
执行代码
再次优化:自动包装类 每次都ResultHelper.success()还要在参数中增加Result来维护一致性总是觉着有点太浪费时间,而且如果忘记了就会出错。所以需要再进一步优化一下,springboot的ResponseBodyAdvice可以实现自动包装类
:::info提示: ResponseBodyAdvice 可以拦截控制器(Controller)方法的返回值,允许我们统一处理返回值或响应体。这对于统一返回格式、加密、签名等场景非常有用。
:::
集成ResponseBodyAdvice接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RestControllerAdvice public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { // boolean supports = returnType.getContainingClass().getPackage().getName().startsWith("com.jianzh5.dailymart"); return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null) { return JsonUtils.obj2String(ResultHelper.success("")); } if (body instanceof String) { // 当响应体是String类型时,使用ObjectMapper转换,因为Spring默认使用StringHttpMessageConverter处理字符串,不会将字符串识别为JSON // return objectMapper.writeValueAsString(ResultFactory.success(body)); return JsonUtils.obj2String(ResultHelper.success(body)); } if (body instanceof Result<?>) { // 已经包装过的结果无需再次包装 return body; } // 对响应体进行包装 return ResultHelper.success(body); } }
再修改一下之前的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RestController @RequestMapping("/user") public class UserController { @PostMapping("/test") public String test(@RequestBody String request) { return (new ServerTemplate<String, String>() { @Override protected void validParam(String request) { } @Override protected String doProcess(String request) { return request; } }).process(request); } }
执行结果
全局异常处理 在构建系统的可靠性时容错处理是重要的考虑方便之一,想要系统更加的健壮全局就需要添加全局异常处理,springboot其实已经做了前期的工作,@RestControllerAdvice注解+@ExceptionHandler可以解决这方面的顾虑
自定义异常的创建和使用 定义自定义异常基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Getter public abstract class AbstractException extends RuntimeException { @Serial private static final long serialVersionUID = 1L; private final String code; private final String message; public AbstractException(ErrorCode errorCode, String message, Throwable throwable) { super(message, throwable); this.code = errorCode.getCode(); this.message = Optional.ofNullable(message).orElse(errorCode.getMessage()); } public AbstractException(String code, String message, Throwable throwable) { super(message, throwable); this.code = code; this.message = message; } }
定义自定义异常类
1 2 3 4 5 6 7 8 9 10 public class BusinessException extends AbstractException { public BusinessException(ErrorCode errorCode, String message, Throwable throwable) { super(errorCode, message, throwable); } public BusinessException(String code, String message, Throwable throwable) { super(code, message, throwable); } }
全局异常的定义 在服务端在接收到参数,通常是不受信任的,这时我们需要对参数进行验证,因为参数验证并不是业务错误所以需要进行额外的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class, ValidationException.class}) public Result<Void> handleValidException(HttpServletRequest request, Exception e) { String exceptionStr = "参数校验异常"; if (e instanceof MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors()); exceptionStr = Optional.ofNullable(firstFieldError) .map(FieldError::getDefaultMessage) .orElse(StrUtil.EMPTY); } else if (e instanceof ConstraintViolationException) { ConstraintViolationException ex = (ConstraintViolationException) e; ConstraintViolation<?> firstConstraintViolation = CollectionUtil.getFirst(ex.getConstraintViolations()); exceptionStr = Optional.ofNullable(firstConstraintViolation) .map(ConstraintViolation::getMessage) .orElse(StrUtil.EMPTY); } else if (e instanceof BindException) { BindException ex = (BindException) e; ObjectError firstObjectError = CollectionUtil.getFirst(ex.getAllErrors()); exceptionStr = Optional.ofNullable(firstObjectError) .map(ObjectError::getDefaultMessage) .orElse(StrUtil.EMPTY); } log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr); return ResultHelper.failure(ErrorCode.PARAMETER_VALIDATION_FAILED.getCode(), exceptionStr); } // 处理自定义异常 @ExceptionHandler(value = {AbstractException.class}) public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) { String requestURL = getUrl(request); log.error("[{}] {} [ex] {}", request.getMethod(), requestURL, ex.toString()); return ResultHelper.failure(ex.getMessage()); } // 兜底处理 @ExceptionHandler(value = Throwable.class) public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) { log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable); return ResultHelper.failure(ErrorCode.SERVICE_ERROR); } // 获取浏览地址信息 private String getUrl(HttpServletRequest request) { if (StrUtil.isEmpty(request.getQueryString())) { return request.getRequestURL().toString(); } return request.getRequestURL().toString() + "?" + request.getQueryString(); } }
测试全局异常的定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("/test") public String test(@RequestBody String request) { return (new ServerTemplate<String, String>() { @Override protected void validParam(String request) { if (request == null){ throw new BusinessException(ErrorCode.CLIENT_ERROR.getCode(), "请求参数不能为空"); } } @Override protected String doProcess(String request) { return request; } }).process(request); } }
小结 上文中实现了统一返回格式以及统一的模板方法,以及为了简化写法的自动包装类和全局异常的处理,在日常开发中每个人的写法各异,统一的写法规定可以避免错误的代码实现以及提升代码的可读性,可以提升整个系统的可靠性,我们可以借助springboot的注解来实现这些在