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

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

相关推荐
Asthenia041236 分钟前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom1 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide1 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04122 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫