《程序猿入职必会(6) · 返回结果统一封装》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍

CSDN.gif

写在前面的话

本系列博文已连载到第六篇,通过前五篇博文,我们已完成了教师信息的基础增删改查功能,在介绍其他知识专栏之前,先来谈一谈CURD页面的规范问题。
前后端分离的开发模式中,后端程序猿有必要与前端程序猿约定一个相对于规范的返回格式,如果仅仅返回数据,有点像裸奔。因此,后端项目需要对返回结果进行统一封装返回,前端也需要封装请求后置拦截器对返回结果处理。
按业内约定俗成的规范,返回结果至少包含:code 状态码、data 数据、msg 消息内容、error 错误内容。
上述只是基础部分,实际开发中,可能还包含:timestamp 时间戳、requestId 日志ID等等。
加油,程序猿,保持住Tempo,开干,玩的就是真实!

关联文章:
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》
《程序猿入职必会(2) · 搭建具备前端展示效果的 Vue》
《程序猿入职必会(3) · SpringBoot 各层功能完善 》
《程序猿入职必会(4) · Vue 完成 CURD 案例 》
《程序猿入职必会(5) · CURD 页面细节规范 》


返回结果统一封装

定义一个返回值VO类

这个是考虑统一封装的第一步,很简单,仅提供参考。

@Data
public class ResultModel<T> {

    /**
     * 成功编码
     */
    public static final String SUCCESS_CODE = ResponseCodeEnum.SUCCESS.getCode();

    /**
     * 异常编码
     */
    public static final String ERROR_CODE = ResponseCodeEnum.EX_ERROR.getCode();

    /**
     * 响应编码
     */
    private String code = SUCCESS_CODE;

    /**
     * 响应数据
     */
    private T data;

    /**
     * 响应信息
     */
    private String message = "";

    /**
     * 异常详细信息
     */
    private String error = "";

    /**
     * 返回成功
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResultModel<T> success(T data) {
        return success(data, "");
    }

    /**
     * 返回成功
     * @param data
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResultModel<T> success(T data, String message) {
        return new ResultModel(SUCCESS_CODE, data, message);
    }

    /**
     * 返回失败
     * @param code
     * @param message
     * @param error
     * @return
     */
    public static ResultModel fail(String code, String message, String error) {
        return new ResultModel(code, null, message, error);
    }

    public static ResultModel fail(ResponseCodeEnum code) {
        return new ResultModel(code.getCode(), null, code.getMessage(), code.getMessage());
    }

    public static ResultModel fail(ResponseCodeEnum code, String error) {
        return new ResultModel(code.getCode(), null, code.getMessage(), error);
    }

    public static ResultModel fail(String error) {
        return new ResultModel(ResponseCodeEnum.EX_ERROR.getCode(), null, error, error);
    }

    public boolean isSuccess() {
        return Objects.equals(this.code, ResponseCodeEnum.SUCCESS.getCode());
    }

    public ResultModel() {
    }

    public ResultModel(String code, T data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }

    public ResultModel(String code, T data, String message, String error) {
        this.code = code;
        this.data = data;
        this.message = message;
        this.error = error;
    }
}

也可以定义一个状态枚举类,非必须:

public enum ResponseCodeEnum {

    /**
     * 调用成功
     */
    SUCCESS("00000", "调用成功"),

    /**
     * 系统异常
     */
    EX_ERROR("EX00000", "系统异常"),

    /**
     * 参数不合法
     */
    EX_PARAM("EX00001", "参数不合法"),

    /**
     * 接口调用异常
     */
    EX_REQUEST("EX00002", "接口调用异常"),

    /**
     * 接口返回错误
     */
    EX_RESULT("EX00003", "接口返回错误"),

    /**
     * 微信接口异常
     */
    EX_WECHAT("EX00004", "微信接口异常"),

    /**
     * 令牌为空
     */
    EX_TOKEN_EMPTY("EX00005", "令牌为空"),

    /**
     * 令牌无效
     */
    EX_TOKEN_INVALID("EX00006", "令牌无效"),

    /**
     * 网站来源无效
     */
    EX_REFERER_INVALID("EX00007", "网站来源无效"),

    /**
     * 404
     */
    EX_PAGE_404("EX404", "页面地址无效");

    private String code;

    private String message;

    ResponseCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "[" + this.code + "]" + this.message;
    }
}

处理返回值的几种方案

SpringBoot 针对 返回值处理有多种方案,相关关键词诸如 ResponseBodyAdvice、MessageConverters、 HandlerMethodReturnValueHandler。

【三者比较】
1、ResponseBodyAdvice(响应拦截器):
作用:ResponseBodyAdvice 允许你在将响应体写入 HTTP 响应之前拦截和修改它。它提供了一种全局定制响应处理逻辑的方式,适用于 Spring MVC 或 Spring WebFlux 应用程序。
工作原理:ResponseBodyAdvice 接口定义了在响应体写入之前将被调用的方法,你可以在这些方法中检查或修改响应体、方法返回类型、请求和其他上下文信息。这使得你可以根据应用程序的需求对响应进行定制化处理。
示例:你可以使用 ResponseBodyAdvice 添加全局的响应头信息、对返回数据进行统一的格式化等。
2、MessageConverters(消息转换器):
作用:MessageConverters 负责将 Controller 方法的返回值转换为 HTTP 响应的内容,以及将请求的内容转换为 Controller 方法的参数。
工作原理:消息转换器负责将 Java 对象与特定的媒体类型之间进行转换,例如 JSON、XML、HTML 等。它可以根据请求的 Content-Type 头信息和方法的返回值类型,选择适当的转换器来进行转换。
示例:你可以使用 MappingJackson2HttpMessageConverter 将 Java 对象转换为 JSON 格式的响应体,或将请求体中的 JSON 数据转换为 Java 对象。
3、HandlerMethodReturnValueHandler(返回值处理器):
作用:HandlerMethodReturnValueHandler 用于处理方法的返回值,将其转换为合适的响应内容。它负责将方法的返回值转换为 HTTP 响应体的内容。
工作原理:HandlerMethodReturnValueHandler 负责将方法的返回值转换为特定的响应内容,例如对象、字符串、视图等。它可以根据返回值的类型和请求的信息来选择适当的处理方式。
示例:你可以使用 ViewMethodReturnValueHandler 将返回值转换为视图,HttpEntityMethodProcessor 将返回的 HttpEntity 对象转换为 HTTP 响应。
总的来说,ResponseBodyAdvice 允许你在响应体写入之前对其进行全局性的处理,MessageConverters 负责将 Java 对象与特定的媒体类型之间进行转换,而 HandlerMethodReturnValueHandler 用于根据方法的返回值类型和请求信息将其转换为合适的响应内容。
关于顺序,HandlerMethodReturnValueHandler 负责处理方法的返回值,ResponseBodyAdvice 在写入响应体之前提供额外的处理机会,而 MessageConverters 则负责将处理过的结果转换为特定的媒体类型。因此,它们的执行顺序是:先执行 HandlerMethodReturnValueHandler,然后是 ResponseBodyAdvice,最后是 MessageConverters。

【方案点评】
三种处理方案各有千秋,本文选用 HandlerMethodReturnValueHandler 展开介绍,顺便可以介绍一下自定义注解的结合使用。
当然,博主所在公司进行框架封装时,采用 ResponseBodyAdvice,并未采用 HandlerMethodReturnValueHandler,原因是,自定义 HandlerMethodReturnValueHandler 意味着要替换 RequestResponseBodyMethodProcessor, SpringMVC 的若干默认定制功能就消失了,可能导致非意料的情况,具体后续再专栏介绍。


HandlerMethodReturnValueHandler

废话不多说,直接上代码。

Step1、定义两个自定义注解,放着备用
后续需要进行返回值封装处理的控制器,就使用@ResultController 注解即可。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResultModelAnnotation {
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@ResultModelAnnotation
public @interface ResultController {
}

Step2、自定义 HandlerMethodReturnValueHandler
实现 HandlerMethodReturnValueHandler 接口,实现 supportsReturnType 和 handleReturnValue 方法。
supportsReturnType 代表生效时机,下方意思是当类或者方法包含 ResultModelAnnotation 注解的时生效。
handleReturnValue 代表返回值处理逻辑,其实就是封装成 ResultModel 格式,再 response 出去。

public class ResultModelHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResultModelAnnotation.class) || returnType.hasMethodAnnotation(ResultModelAnnotation.class));
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        ResultModel<Object> resultModel;
        ApiOperation methodAnnotation = returnType.getMethodAnnotation(ApiOperation.class);
        String message = "";
        if (methodAnnotation != null) {
            message = methodAnnotation.value() + "成功";
        }

        if (returnValue instanceof ResultModel) {
            resultModel = (ResultModel<Object>) returnValue;
            if (!resultModel.isSuccess()) {
                resultModel.setMessage(message + "error");
            }
        } else {
            resultModel = ResultModel.success(returnValue, message);
        }
        mavContainer.setRequestHandled(true);
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        // 设置状态码
        response.setStatus(HttpStatus.OK.value());
        response.setHeader("result-model", "true");
        // 设置ContentType
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        // 避免乱码
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.write(JSON.toJSONString(resultModel, SerializerFeature.WriteMapNullValue));
            writer.flush();
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

Step3、自定义RequestMappingHandlerAdapter
继承 RequestMappingHandlerAdapter,重写 afterPropertiesSet 方法。
逻辑就是将前面自定义的 ResultModelHandlerMethodReturnValueHandler,放到第一位,首发选手。

public class ResultRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        List<HandlerMethodReturnValueHandler> returnValueHandlers = super.getReturnValueHandlers();
        ResultModelHandlerMethodReturnValueHandler handler = new ResultModelHandlerMethodReturnValueHandler();
        List<HandlerMethodReturnValueHandler> list = new ArrayList<>();
        list.add(handler);
        list.addAll(returnValueHandlers);
        super.setReturnValueHandlers(list);
    }
}

Step4、控制类添加自定义注解
直接用前面博文提到的教师信息控制器,将 @RestController 注解修改为 @ResultController

Tips:若部分接口不需要按这个格式返回,则不需要修改注解。

@ResultController
@Api(value = "ZyTeacherInfoController", tags = {"教师信息表服务"})
@RequestMapping(value = "/zyTeacherInfo")
public class ZyTeacherInfoController extends BaseController {
    
}

Step5、万事俱备,测试一下
启动服务,访问单个教师的接口:http://localhost:8083/zyTeacherInfo/2
输出信息如下,可以看到其格式了,搞定收工!

{
  "code": "00000",
  "data": {
    "createdTime": "2024-05-16 20:07:21",
    "modifiedTime": null,
    "sortNo": null,
    "stuItem": null,
    "teaCode": "2",
    "teaConfig": null,
    "teaImg": null,
    "teaName": "李老师",
    "teaPhone": null,
    "teaType": null,
    "validFlag": "1"
  },
  "error": "",
  "message": "获取教师信息表详细信息成功",
  "success": true
}

总结陈词

此篇文章介绍了前后端分离项目中,关于统一返回结果的封装,仅供学习参考。
下一篇文章介绍前端 Axios 插件封装思路,以及对于这一返回封装结果的接受处理。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

CSDN_END.gif

  • 37
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战神刘玉栋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值