Spring Boot 统一接口响应格式的正确姿势

2024-08-06 10:40:40 浏览数 (1)

01、背景介绍

熟悉 web 系统开发的同学可能比较熟悉,目前绝大多数的互联网软件平台基本都是前后端分离的开发模式,为了加快前后端接口对接速度,一套完善且规范的接口标准格式是非常有必要的,不仅能够提升开发效率,也会让代码看起来更加简洁、好维护。

今天这篇文章,我们一起来学习一下如何在 Spring Boot 中统一接口的返回数据格式。

02、定义数据返回格式

最常见的一种做法是封装一个工具类,在类中定义需要返回的字段信息,比如状态码、结果描述、结果数据集等,然后在接口中返回给客户端。

例如如下示例。

2.1、定义返回对象

public class ResultMsg<T> { /**状态码**/ private int code; /**结果描述**/ private String message; /**结果集**/ private T data; /**时间戳**/ private long timestamp; // set、get方法等... public ResultMsg() { timestamp = System.currentTimeMillis(); } public static <T> ResultMsg<T> success() { return success(null); } public static <T> ResultMsg<T> success(T data) { ResultMsg<T> resultMsg = new ResultMsg<>(); resultMsg.setCode(ReturnCode.RC200.getCode()); resultMsg.setMessage(ReturnCode.RC200.getMessage()); resultMsg.setData(data); return resultMsg; } public static <T> ResultMsg<T> fail(String message) { return fail(ReturnCode.RC500.getCode(), message); } public static <T> ResultMsg<T> fail(ReturnCode returnCode) { return fail(returnCode.getCode(), returnCode.getMessage()); } public static <T> ResultMsg<T> fail(int code, String message) { ResultMsg<T> resultMsg = new ResultMsg<>(); resultMsg.setCode(code); resultMsg.setMessage(message); return resultMsg; } }

2.2、定义状态码

public enum ReturnCode { /**操作成功**/ RC200(200,"请求成功"), /**access_denied**/ RC403(403,"无访问权限,请联系管理员授予权限"), /**服务异常**/ RC500(500,"系统异常,请稍后重试"); /**自定义状态码**/ private final int code; /**自定义描述**/ private final String message; ReturnCode(int code, String message){ this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }

2.3、统一返回格式

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

当请求http://localhost:8080/queryUser?userId=1地址时,返回结果示例如下:

{ "code": 200, "message": "请求成功", "data": { "userId": 1, "userName": "张三" }, "timestamp": 1716540843798 }

前端根据code值来判断接口请求是否成功。

这种实现方式属于万能的一种方式,唯一的缺点就是重复编程工作很大,每个接口都要根据业务的要求进行不同程度的try..catch操作,如果有几百个接口,代码看起来会比较臃肿。

03、高级封装实现

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

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

3.1、ResponseBodyAdvice 用法介绍

/** * 对controller 层中 ResponseBody 注解方法,进行增强拦截 */ @ControllerAdvice public class CustomerResponseAdvice implements ResponseBodyAdvice<Object> { /** * 是否开启支持功能 */ @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } /** * 如果开启,就会对返回结果进行处理 */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { return ResultMsg.success(body); } }

3.2、调整接口返回参数

此时,在controller层无需返回ResultMsg对象类型,直接改成对应的业务对象即可,示例如下:

@RestController public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/queryUser") public User query(@RequestParam("userId") Long userId){ // 业务代码... User user = userService.queryId(userId); return user; } }

在此请求接口地址,返回结果与上文一致。

3.3、字符串类型返回问题

假如返回的接口数据对象是字符串类型,比如如下示例:

@RestController public class HelloController { @GetMapping(value = "/hello") public String hello(){ return "hello world"; } }

先猜猜结果如下?

在浏览器中请求地址http://localhost:8080/hello,结果如下:

抛出异常了!错误原因如下。

java.lang.ClassCastException: com.example.basic.core.result.ResultMsg cannot be cast to java.lang.String

发生这个现象的原因在于:当接口返回的结果是String类型时,会优先使用StringHttpMessageConverter字符串消息转换器来响应数据,其次采用对象转换器。

因此我们需要对CustomerResponseAdvice进行改造,当返回的数据类型为String时,对其单独进行处理,示例如下:

/** * 如果开启,就会对返回结果进行处理 */ @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 设置响应类型为json response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); if(body != null && (body instanceof ResultMsg)){ // 如果body返回的是ResultMsg类型的对象,不进行增强处理 return body; } if(body != null && (body instanceof String)){ // 如果body返回的是String类型的对象,单独处理 return toJson(body); } return ResultMsg.success(body); } private Object toJson(Object body) { try { return new ObjectMapper().writeValueAsString(ResultMsg.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException("无法转发json格式", e); } }

启动服务后,再次请求地址,结果如下:

从日志上可以清晰的看到,与预期一致!

**有个地方需要重点注意一下:默认String类型的数据响应给客户端的格式为text/html,为了统一响应格式,需要手动设置响应类型为json**。

3.4、全局异常处理

在上文的介绍中,当遇到异常时第一时间想到的是try...catch。其实大量的try...catch,不仅编程工作量很大,而且可读性也差。

在 Spring Boot 中,其实我们不用一个一个的去写,我们可以利用@ControllerAdvice和@ExceptionHandler注解实现全局异常处理器,拦截controller层抛出的异常,具体应用示例如下:

@ControllerAdvice public class GlobalExceptionHandler { /** * 处理全局异常 * @param e * @return */ @ExceptionHandler(value = Exception.class) @ResponseBody public Object exceptionHandler(Exception e){ // 业务异常 if(e instanceof BusinessException){ BusinessException ex = (BusinessException) e; return ResultMsg.fail(e.getCode, ex.getMessage()); } // 运行时异常 if(e instanceof RuntimeException){ RuntimeException ex = (RuntimeException) e; return ResultMsg.fail(500, ex.getMessage()); } return ResultMsg.fail(999, e.getMessage()); } }

下面我们写一个异常接口来验证一下,代码如下:

@GetMapping(value = "/fail") public String fail(){ if(1 == 1){ throw new RuntimeException("测试"); } return "1"; }

启动服务后,在浏览器发起请求,返回结果如下:

04、小结

最后总结一下,在实际的项目开发过程中,统一响应格式通常有两种实现方式。

  • 方式一:在接口层直接返回标准格式,同时通过全局异常处理器来捕捉并处理异常;
  • 方式二:在接口层返回业务对象,通过实现ResponseBodyAdvice接口统一封装格式

如果不希望 Spring Boot 托管响应内容,要求编程风格统一,可以采用方式一;如果希望尽量简化业务代码的开发量,可以采用方式二。

这两种方式,项目开发中都有所应用,具体采用哪一种,通常以团队开发风格为主。

0 人点赞