企业宣传,产品推广,广告招商,广告投放联系seowdb

API 定义优雅全局统一 太优雅了! Restful 响应和统一异常处理 SpringBoot

SpringBoot 定义优雅全局统一 Restful API 响应和统一异常处理,太优雅了!

统一接口响应能够减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。

大家好,我是码哥,《Redis 高手心法》作者。

假如你作为项目组长,为 Spring Boot 项目设计一个规范的统一的RESTfulAPI响应框架。

前端或者移动端开发人员通过调用后端提供的RESTful接口完成数据的交换。

常见的统一响应数据结构如下所示:

public class Result<T> implements Serializable {private Integer code;private String message;private T>

统一接口响应能够减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。

除此之外,还需要实现一个统一的异常处理框架。通过这个全局异常处理,可以避免将异常信息和系统敏感信息直接抛出给客户端。

针对特定异常捕获后可以重新对异常输出信息做编排,提高交互友好度,同时可以记录异常信息。

我们需要定义一个Result类,在类中定义需要返回的字段信息,比如状态码、结果描述、结果数据集等。

接口的状态码很多,我们可以用一个枚举类进行封装。于是就有了下面的代码。顺便说一句,推荐大家使用lombok,减少繁琐的 set、get、构造方法。

@Getter@AllArgsConstructorpublic enum ResultEnum {/*** return success result.*/SUCCESS(200, "接口调用成功"),/*** return business common failed.*/COMMON_FAILED(, "接口调用失败"),NOT_FOUND(404, "接口不存在"),FORBIDDEN(403, "资源拒绝访问"),UNAUTHORIZED(401, "未认证(签名错误)"),INTERNAL_SERVER_ERROR(500, "服务器内部错误"),NULL_POINT(200002, "空指针异常"),PARAM_ERROR(200001, "参数错误");private Integer code;private String message;}

封装一个固定返回格式的结构对象:Result。

@Setter@Getterpublic class Result<T> implements Serializable {private Integer code;private String message;private T>

有了统一响应体,于是你就可以在 Controller 返回结果时这样写:

@RestControllerpublic class UserController {@Autowiredprivate UserService userService;@RequestMapping(value = "/queryUser")public Result<User> query(@RequestParam("userId") Long userId){try {// 业务代码...User user = userService.queryId(userId);return ResultMsg.success(user);} catch (Exception e){return ResultMsg.fail(e.getMessage());}}}

这个问题问得好。

为了能够实现统一的响应对象,又能优雅的定义 Controller 类的方法,使其每个方法的返回值是其应有的类型。

主要是借助RestControllerAdvice注解和ResponseBodyAdvice接口来实现对接口响应给客户端之前封装成 Result。

Spring Boot 框架其实已经帮助开发者封装了很多实用的工具,比如 ResponseBodyAdvice 接口,我们可以利用来实现数据格式的统一返回。

有些场景下我们不希望 Controller 方法的返回值被包装为统一响应对象,可以先定义一个忽略响应封装的注解,配合后续代码实现。

@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface IgnoreRestFulAPI {}

这是 Spring 框架提供的一个接口,我们可以利用它实现对接口数据格式统一封装。

ResponseBodyAdvice可以对 controller 层中的拥有@ResponseBody注解属性的方法进行响应拦截,用户可以利用这一特性来封装数据的返回格式,也可以进行加密、签名等操作。

实现该接口的类还需要添加@RestControllerAdvice注解,这是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component。

本质上就是使用 Spring AOP 定义的一个切面,作用于 Controller 方法执行完成后的增强操作。

ResponseBodyAdvice接口有两个方法需要重写。

@RestControllerAdvice@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {private static final ObjectMapper mapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 方法没有IgnoreRestFulAPI注解,且返回类型不是 Result类型时调用 beforeBodyWrite 实现响应数据封装return !returnType.hasMethodAnnotation(IgnoreRestFulAPI.class)&& !returnType.getParameterType().isAssignableFrom(Result.class);}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果返回值是void类型,直接返回200状态信息if (returnType.getParameterType().isAssignableFrom(Void.TYPE)) {return Result.success();}// 返回类型不是 Result,是 String 类型if (!(body instanceof Result)) {// warning: RestController方法上返回值类型为String时,默认响应的Content-Type是text/plain,// 需要手动指定为application/json 才能对结果进行包装成 jsonif (body instanceof String) {return toJson(Result.success(body));}return Result.success(body);}// 返回类型是 Result,直接返回return body;}private Object toJson(Object body) {try {return mapper.writeValueAsString(body);} catch (JsonProcessingException e) {throw new RuntimeException("无法转发json格式", e);}}}

因为启动类上的@SpringbootApplication默认扫描本包和子包。

比如GlobalResponseAdvice在 zero.magebyte.shop.common 包下,而启动类在 zero.magebyte.shop.order.server 包,那么 GlobalResponseAdvice 就不会生效。

为了防止全局接口统一响应处理器GlobalResponseAdvice类未被扫描到,建议在启动类上加上包扫描。

定义一个 Controller 类来进行简单的开发和测试。

@RestController@RequestMapping("/demo")public class DemoController {@GetMapping("/method1")public Result<Integer> method1() {return Result.success(100);}@GetMapping("/method2")public void method2() {}@GetMapping(value = "/method3")@IgnoreRestFulAPIpublic String method3() {return "不会被封装,直接返回 String";}/*** RestController中返回值类型是String的方法默认响应类型是text/plain,需要手动指定为application/json方可对其进行包装*/@GetMapping(value = "/method4", produces = MediaType.APPLICATION_JSON_VALUE)public String method4() {return "会被封装 Result 结构 JSON";}/*** 会被封装,但是响应类型是text/plain* @return*/@GetMapping(value = "/method5")public String method5() {return "会被封装为 Result 的 text/html";}}

method1 方法返回类型是 Result,所以不会再次封装,而是直接返回 Result 结构,并以Content-Type: application/json格式响应给客户端。

{"code": 200,"message": "接口调用成功","data": 100}

method2 方法返回类型是 void,会封装成 Result 结构,并以Content-Type: application/json格式响应给客户端。只不过>

{"code": 200,"message": "接口调用成功","data": null}

method3 被 @IgnoreRestFulAPI 注解,不会被封装 Result 结构,直接返回。

默认 String 类型的数据响应给客户端的格式为text/html,为了统一响应格式,需要手动设置响应类型为 json,如下所示。

@GetMapping(value = "/method4", produces = MediaType.APPLICATION_JSON_VALUE)

响应给客户端的格式就是一个 Result JSON 对象,Content-Type: application/json。

{"code": 200,"message": "接口调用成功","data": "会被封装 Result 结构 JSON"}

否则将会以Content-Type: text/html;charset=UTF-8响应呵客户端。

另外需要注意的是,如果你使用了 swagger,以上代码会导致 swagger 无法访问。

报错如下:

Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available atthen the base url isPlease enter the location manually:

原因:因为统一响应拦截器对 swagger 的接口做了拦截并对结果做了包装,导致返回结构发生后变化,swagger 无法解析。

解决方案:修改统一响应处理器拦截的范围,配置散列包路径。你可以指定@RestControllerAdvice(basePackages = {"xxx.xxx"})项目的 controller 目录即可。

@RestControllerpublic class UserController {@Autowiredprivate UserService userService;@RequestMapping(value = "/queryUser")public User query(@RequestParam("userId") Long userId){try {// 业务代码...User user = userService.queryId(userId);return user;} catch (Exception e){return Result.fail(e.getMessage());}}}

兵来将挡,水来土掩。这样写代码并不是不好看,而是十分垃圾!!!

如下是我们自定义的业务异常。

@Setter@Getterpublic class BusinessException extends RuntimeException {private Integer code;private String message;public BusinessException(Throwable cause) {super(cause);}public BusinessException(String message) {super(message);this.message = message;}public BusinessException(Integer code, String message, Throwable cause) {super(cause);this.code = code;this.message = message;}}

在 Spring Boot 中,我们不用这样写,可以继续利用 @RestControllerAdvice 注解和 @ExceptionHandler 注解实现全局异常处理器,拦截 Controller 层抛出的异常。

新增GlobalExceptionHandler类,编写统一异常处理,类上面添加@RestControllerAdvice注解就开启了全局异常处理。

我们可以在类面创建多个方法,并在方法上添加@ExceptionHandler注解,对不同的异常进行定制化处理,并统一返回 Result 结构响应给客户端。

@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {/*** 处理自定义的业务异常** @param req* @param e* @return*/@ExceptionHandler(value = BusinessException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public <T> Result<T> baseExceptionHandler(HttpServletRequest req, BusinessException e) {log.error("发生业务异常!", e);int code = Objects.isNull(e.getCode()) ? ResultEnum.INTERNAL_SERVER_ERROR.getCode() : e.getCode();String message = StringUtils.isBlank(e.getMessage()) ? ResultEnum.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage();return new Result<>(code, message);}@ExceptionHandler(value = RuntimeException.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public <T> Result<T> runtimeExceptionHandler(HttpServletRequest req, RuntimeException e) {log.error("发生运行时异常!", e);return Result.failed(ResultEnum.INTERNAL_SERVER_ERROR);}/*** 处理空指针的异常** @param req* @param e* @return*/@ExceptionHandler(value = NullPointerException.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public <T> Result<T> exceptionHandler(HttpServletRequest req, NullPointerException e) {log.error("发生空指针异常!", e);return Result.failed(ResultEnum.INTERNAL_SERVER_ERROR);}/*** 处理其他异常** @param req* @param e* @return*/@ExceptionHandler(value = Exception.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public <T> Result<T> exceptionHandler(HttpServletRequest req, Exception e) {log.error("未知异常!", e);return Result.failed(ResultEnum.INTERNAL_SERVER_ERROR);}@ExceptionHandler(value = BindException.class)@ResponseBody@ResponseStatus(HttpStatus.BAD_REQUEST)public Result<String> handlerBindException(HttpServletRequest request, BindException e) {StringBuilder sb = new StringBuilder();List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();for (FieldError fe : fieldErrors) {sb.append(fe.getField()).append(":").append(fe.getDefaultMessage()).append(";");}String errorStr = sb.length() == 0 ? "" : sb.substring(0, sb.length() - 1);return new Result(HttpStatus.BAD_REQUEST.value(), errorStr);}@ExceptionHandler(value = MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public Result<String> handlerMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {StringBuilder sb = new StringBuilder();List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();for (FieldError fe : fieldErrors) {sb.append(fe.getField()).append(":").append(fe.getDefaultMessage()).append(";");}String errorStr = sb.isEmpty() ? "" : sb.substring(0, sb.length() - 1);return new Result<String>(HttpStatus.BAD_REQUEST.value(), errorStr);}@ExceptionHandler(value = SQLException.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Result<String> handlerSQLException(SQLException e) {log.error("数据库异常!", e);return Result.failed(ResultEnum.INTERNAL_SERVER_ERROR);}}

故意制造一个除 0 异常。

@GetMapping(value = "/method6")public Order method6() {int a = 1/0;Order order = new Order();order.setId(1);order.setMoney(999);return order;}

自定义抛出业务异常。

@GetMapping(value = "/method7")public Order method7() {Order order = new Order();order.setId(1);order.setMoney(999);if (order.getCreateTime() == null) {throw new BusinessException("创建时间不能为空");}return order;}

RestControllerAdvice注解和ResponseBodyAdvice接口来实现对接口响应给客户端之前封装成 Result。

统一接口响应客户端,减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。

通过@RestControllerAdvice注解和@ExceptionHandler` 注解实现统一异常处理,能够减少代码的重复度和复杂度,有利于代码的维护,并且能够快速定位到 BUG,大大提高我们的开发效率。

© 版权声明
评论 抢沙发
加载中~
每日一言
不怕万人阻挡,只怕自己投降
Not afraid of people blocking, I'm afraid their surrender