近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。本项目为前后端分离开发,后端基于Java21
和SpringBoot3
开发,后端使用Spring Security
、JWT
、Spring Data JPA
等技术栈,前端提供了vue
、angular
、react
、uniapp
、微信小程序
等多种脚手架工程。
项目地址:https://gitee.com/breezefaith/fast-alden
在前后端分离的项目开发过程中,我们通常会对数据返回格式进行统一的处理,这样可以方便前端人员取数据。但如果定义好响应对象R
后,Controller类中每一个方法的返回值类型都只能是这个响应对象类,会使代码显得很不优雅。
@RestController
@RequestMapping("/admin")
public class AdminController {
@PostMapping(value = "/register")
public R<UmsAdmin> register(@Validated @RequestBody UmsAdminParam umsAdminParam) {
return R.success(new UmsAdmin());
}
@PostMapping(value = "/logout")
public R logout() {
return R.success(null);
}
@PostMapping(value = "/login")
public R login() {
return R.success(new UmsAdmin());
}
}
为了能够实现统一的响应对象,又能优雅的定义Controller类的方法,使其每个方法的返回值是其应有的类型,可以参考本文,主要是借助RestControllerAdvice
注解和ResponseBodyAdvice
接口来实现。
/**
* 响应结果类
*
* @param 任意类型
*/
@Data
public class ResponseResult<T> {
/**
* 响应状态码,200是正常,非200表示异常
*/
private int status;
/**
* 异常编号
*/
private String errorCode;
/**
* 异常信息
*/
private String message;
/**
* 响应数据
*/
private T data;
public static <T> ResponseResult<T> success() {
return success(HttpServletResponse.SC_OK, null, null);
}
public static <T> ResponseResult<T> success(T data) {
return success(HttpServletResponse.SC_OK, null, data);
}
public static <T> ResponseResult<T> fail(String message) {
return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, null);
}
public static <T> ResponseResult<T> fail(String errorCode, String message) {
return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorCode, message, null);
}
public static <T> ResponseResult<T> success(int status, String message, T data) {
ResponseResult<T> r = new ResponseResult<>();
r.setStatus(status);
r.setMessage(message);
r.setData(data);
return r;
}
public static <T> ResponseResult<T> fail(int status, String errorCode, String message) {
return fail(status, errorCode, message, null);
}
public static <T> ResponseResult<T> fail(int status, String errorCode, String message, T data) {
ResponseResult<T> r = new ResponseResult<>();
r.setStatus(status);
r.setErrorCode(errorCode);
r.setMessage(message);
r.setData(data);
return r;
}
}
有些场景下我们不希望Controller方法的返回值被包装为统一响应对象,可以先定义一个忽略响应封装的注解,配合后续代码实现。
/**
* 忽略响应封装注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreRestControllerResponseAdvice {
}
本步骤需要使用@RestControllerAdvice
注解,它是一个组合注解,由@ControllerAdvice
、@ResponseBody
组成,而@ControllerAdvice
继承了@Component
,因此@RestControllerAdvice
本质上是个Component
,用于定义@ExceptionHandler
,@InitBinder
和@ModelAttribute
方法,适用于所有使用@RequestMapping
方法。
还要用到ResponseBodyAdvice
,它是Spring框架提供的一个接口,用于对Controller方法返回的响应体进行全局处理。它可以在Controller方法执行完毕并且响应体已经生成之后,对响应体进行自定义的修改或者增强操作。它本质上就是使用Spring AOP定义的一个切面,作用于Controller方法执行完成后。
具体实现代码如下:
/**
* 响应实体封装切面
*/
@RestControllerAdvice(basePackages = {"com.demo.controller"})
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 方法没有IgnoreRestControllerResponseAdvice注解,且response不是ResponseResult类型时启用beforeBodyWrite
return !returnType.hasMethodAnnotation(IgnoreRestControllerResponseAdvice.class)
&& !returnType.getParameterType().isAssignableFrom(ResponseResult.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 如果返回值是void类型,直接返回200状态信息
if (returnType.getParameterType().isAssignableFrom(void.class)) {
return ResponseResult.success();
}
if (!(body instanceof ResponseResult)) {
// warning: RestController方法上返回值类型为String时,响应的Content-Type是text/plain,需要手动指定为application/json
if (body instanceof String) {
try {
return JsonUtils.toJSON(ResponseResult.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return ResponseResult.success(body);
}
return body;
}
}
上述代码会对com.demo.controller
包下所有的含有@RequestMapping
注解的方法进行拦截,如果方法上没有IgnoreRestControllerResponseAdvice
注解且返回值类型不是ResponseResult
时,执行beforeBodyWrite
方法。在beforeBodyWrite
中将方法返回值包装为ResponseResult
对象。
下面我们就可以定义一个Controller类来进行简单的开发和测试。
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/method1")
public ResponseResult<Integer> method1() {
return ResponseResult.success(100);
}
@GetMapping("/method2")
public void method2() {
}
@GetMapping(value = "/method3")
@IgnoreRestControllerResponseAdvice
public String method3() {
return "不会被封装";
}
/**
* RestController中返回值类型是String的方法默认响应类型是text/plain,需要手动指定为application/json方可对其进行包装
*/
@GetMapping(value = "/method4", produces = MediaType.APPLICATION_JSON_VALUE)
public String method4() {
return "会被封装";
}
}
本文介绍了SpringBoot项目中优雅地实现统一响应对象,如有错误,还望批评指正。
在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。