SpringBoot中优雅地实现统一响应对象

目录

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。本项目为前后端分离开发,后端基于Java21SpringBoot3开发,后端使用Spring SecurityJWTSpring Data JPA等技术栈,前端提供了vueangularreactuniapp微信小程序等多种脚手架工程。

项目地址:https://gitee.com/breezefaith/fast-alden

在前后端分离的项目开发过程中,我们通常会对数据返回格式进行统一的处理,这样可以方便前端人员取数据。但如果定义好响应对象R后,Controller类中每一个方法的返回值类型都只能是这个响应对象类,会使代码显得很不优雅。

java 复制代码
@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接口来实现。

实现步骤

定义统一响应对象类

java 复制代码
/**
 * 响应结果类
 *
 * @param <T> 任意类型
 */
@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方法的返回值被包装为统一响应对象,可以先定义一个忽略响应封装的注解,配合后续代码实现。

java 复制代码
/**
 * 忽略响应封装注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreRestControllerResponseAdvice {
}

实现ResponseBodyAdvice接口

本步骤需要使用@RestControllerAdvice注解,它是一个组合注解,由@ControllerAdvice@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler@InitBinder@ModelAttribute方法,适用于所有使用@RequestMapping方法。

还要用到ResponseBodyAdvice,它是Spring框架提供的一个接口,用于对Controller方法返回的响应体进行全局处理。它可以在Controller方法执行完毕并且响应体已经生成之后,对响应体进行自定义的修改或者增强操作。它本质上就是使用Spring AOP定义的一个切面,作用于Controller方法执行完成后。

具体实现代码如下:

java 复制代码
/**
 * 响应实体封装切面
 */
@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类

下面我们就可以定义一个Controller类来进行简单的开发和测试。

java 复制代码
@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项目中优雅地实现统一响应对象,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

相关推荐
老友@1 天前
一次由 PageHelper 分页污染引发的 Bug 排查实录
java·数据库·bug·mybatis·pagehelper·分页污染
AI分享猿1 天前
小白学规则编写:雷池 WAF 配置教程,用 Nginx 护住 WordPress 博客
java·网络·nginx
sp421 天前
漫谈 Java 轻量级的模板技术:从字符串替换到复杂模板
java·后端
2301_795167201 天前
玩转Rust高级应用. ToOwned trait 提供的是一种更“泛化”的Clone 的功能,Clone一般是从&T类型变量创造一个新的T类型变量
开发语言·后端·rust
952361 天前
数据结构-链表
java·数据结构·学习
喵手1 天前
Java线程通信:多线程程序中的高效协作!
java
草莓熊Lotso1 天前
C++ 方向 Web 自动化测试实战:以博客系统为例,从用例到报告全流程解析
前端·网络·c++·人工智能·后端·python·功能测试
TDengine (老段)1 天前
TDengine 字符串函数 CONCAT 用户手册
java·数据库·tdengine
one year.1 天前
Linux:线程同步与互斥
java·开发语言
一 乐1 天前
旅游|内蒙古景点旅游|基于Springboot+Vue的内蒙古景点旅游管理系统设计与实现(源码+数据库+文档)
开发语言·前端·数据库·vue.js·spring boot·后端·旅游