搞懂 SpringBoot 统一返回结果的实现


开发背景

现如今前后端分离已经是项目开发的主流方式,在前后端分离开发情形下,少不了前端和后端之间的友好交流,为了避免上升为物理交流,项目中必须要有一套规范有效的前后端协议格式。

后端开发的不同服务、不同业务处理并返回不同类型的数据,这不仅会增加巨大工作量来进行协议的输出,数据格式的多样化对于前端同事来讲也是一个灾难,这就需要对后端服务接口的返回格式定义成统一规范的结果类型。

前后端开发过程中数据交互规范化是一件非常重要的事情,不仅可以减少前后端交互过程中出现的问题,也让代码逻辑更加具有条理。

初始篇:从封装返回结果说起

返回结果类基本特征

对于后端的返回数据,考虑将格式统一后返回,在开发大量后端服务接口之后,根据开发经验可以总结得到,请求一个接口时需要关注的指标有:

  • 响应状态码,即请求接口返回状态码,如 HTTP 请求中的 200、304、500 等状态
  • 响应结果描述,有些接口请求成功或失败需要返回描述信息供前端展示
  • 响应结果数据,大部分的接口都会返回后端获取的数据,并以列表的形式展示的前端页面中
  • 是否成功:在实际项目中请求接口时,首先要关注的应该是接口的请求是否成功,然后才会去关注成功返回数据或者错误代码和信息,在统一数据中可以加入请求是否成功的标识,当然接口的成功与否也可以根据状态码可以判断,可以根据实际需求考虑是否定义结果状态
  • 其他标识:为了显示更多接口调用的信息,可能会根据实际的业务需求加入接口调用的时间信息等。

除了以上属性特征外,返回结果类在定义时还应该满足:

  1. 属性私有化,使用 get/set 方法来操作属性值
  2. 构造器私有化,外部只可以调用方法,初始化要在类内部完成
  3. 由于外部需要直接调用方法,因此方法要定义为静态方法

松散的自定义返回结果

根据上述对返回结果基本特征的分析,我们可以定义一个如下代码所示为的返回结果类

java 复制代码
public class Result {
    private Integer code;
    private String desc;
    private Object data;
    
    // 是否请求成功,本文使用 code = 10000 特指成功,其余为失败,因此不再冗余 success
    // private Boolean success;
    //请求时间,暂时不需要,可根据需求定义
    //private long timestamp;
    
    //构造器私有
    private Result() {}
    
    //get/set 方法
    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public String getCode() {
        return code;
    }

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    
    /**
     * 返回通用成功
     * @return Result
     */
    public static Result ok(){
        Result result = new Result();
        result.setSuccess(true);
        result.setCode("20000");
        result.setDesc("请求成功");
        return result;
    }

    /**
     * 返回通用失败,未知错误
     * @return Result
     */
    public static Result error(){
        Result result = new Result();
        result.setSuccess(false);
        result.setCode(20001);
        result.setDesc("请求失败");
        return result;
    }
}

lombok:代码简洁利器

为了减少 get/set 等代码内容,引入了 lombok 工具,并使用注解 @Data 标注,代表当前类默认生成 set/get 方法

java 复制代码
@Data
public class Result {
    private Integer code;
    private String desc;
    private Object data;
    
    private Result() {}
    
    /**
     * 返回通用成功
     * @return Result
     */
    public static Result ok(){
        Result result = new Result();
        result.setCode("20000");
        result.setDesc("请求成功");
        return result;
    }

    /**
     * 返回通用失败,未知错误
     * @return Result
     */
    public static Result error(){
        Result result = new Result();
        result.setCode(20001);
        result.setDesc("请求失败");
        return result;
    }
}

结果类使用方法

定义返回结果类后,Controller 对应的服务方法中就可以使用其作为返回结果类型,如下

java 复制代码
@PostMapping("get")
public String getInfo(){
    // 处理逻辑在这里
    String result = "返回结构或";
    
    // 封装返回结果
    Result result = Result.ok();
    result.setData(result);
    return result;
}

进阶篇:枚举错误类和链式返回来加盟

实际面临的小问题

上述返回结果定义内容,尽管满足了基本需求,但是在使用时仍存在着如下的问题

  1. 返回结果需要先初始化,然后再进行结果赋值处理,相当于返回值仍需要手动添加,这样即增加了数据错误的风险,并且并没有减少实际代码量,不能凸显统一封装带来的好处。
  2. 上述封装类中,分别定义了返回成功和失败的静态方法,但是对于失败的结果可能是多样的,不可能针对每种失败分别定义对应的静态方法,这样即繁琐又不现实。

为了解决上述问题,对现有的结果类进行优化处理,采用方法有

  1. 使用返回对象本身的方式来简化对象初始化和赋值步骤,简洁代码,突出重点
  2. 采用返回结果枚举类的方式将所有可能返回的结果定义为枚举类常量,在返回结果类中使用对应的枚举类返回创建,以此处理异常结果多样性问题

定义返回结果枚举类

首先定义返回结果枚举类,枚举类的使用可以进一步规范返回结果类中定义的属性取值。

java 复制代码
@Getter
public enum ResultCodeEnum {
    SUCCESS(20000,"响应成功"),
    UNKNOWN_ERROR(20001,"未知错误"),
    PARAM_ERROR(20002,"参数错误"),
    NULL_POINT_ERROR(20003,"空指针异常"),
    HTTP_CLIENT_ERROR(20003,"客户端连接异常");
    
    /**
     * 响应状态码
     */
    private Integer code;

    /**
     * 响应描述信息
     */
    private String desc;
    
    ResultCodeEnum(Integer code, String desc){
        this.code = code;
        this.desc = desc;
    }
}

@Getter 注解也是 lombok 提供的注解,代表为当前类属性仅生成 get 方法,枚举类不需要 set 方法,属性赋值通过定义枚举对象或者构造方法实现。

状态枚举以及链式返回实现

实现链式返回需要定义属性的 set 方法返回结果类型为当前结果类,并在方法中返回对象本身 this

java 复制代码
@Data
public class Result {
    private Integer code;
    private String desc;
    private Object data;
    
    private Result() {}
    
    /**
     * 使用枚举类设置返回结果
     * @param resultCodeEnum
     * @return
     */
    public static Result setResult(ResultCodeEnum resultCodeEnum){
        Result result = new Result();
        result.setCode(resultCodeEnum.getCode());
        result.setDesc(resultCodeEnum.getDesc());
        return result;
    }
    
     /**
     * 返回通用成功
     * @return Result
     */
    public static Result ok(){
        // 链式处理
        return new Result().setResult(ResultCodeEnum.SUCCESS);
    }

    /**
     * 返回通用失败,未知错误
     * @return Result
     */
    public static Result error(){
        // 链式处理
        return new Result().setResult(ResultCodeEnum.UNKNOWN_ERROR);
    }
    
    /**
     * 返回结果类,使用链式编程
     * 自定义成功标识
     * @param 
     * @return
     */
     public Result setSuccess(Boolen success){
        this.setSuccess(success);
        return this;
    }
    
    /**
     * 返回结果类,使用链式编程
     * 自定义状态码
     * @param 
     * @return
     */
     public Result setCode(Integer code){
        this.setCode(code);
        return this;
    }
    
    /**
     * 返回结果类,使用链式编程
     * 自定义返回结果描述
     * @param 
     * @return
     */
     public Result setDesc(String desc){
        this.setDesc(desc);
        return this;
    }
    
    /**
     * 返回结果类,使用链式编程
     * 自定义结果数据
     * @param 
     * @return
     */
     public Result setData(Object data){
        this.setData(data);
        return this;
    }
     
}

lombok:我又来了

对于链式返回的处理 lombok 也提供了一个 @Accessors(chain = true) 代表为 set 方法实现链式返回结构,使用注解实现如下

java 复制代码
@Data
@Accessors(chain = true)
public class Result {
    private Integer code;
    private String desc;
    private Object data;
    
    private Result() {}
    
    /**
     * 使用枚举类设置返回结果
     * @param resultCodeEnum
     * @return
     */
    public static Result setResult(ResultCodeEnum resultCodeEnum){
        Result result = new Result();
        result.setCode(resultCodeEnum.getCode());
        result.setDesc(resultCodeEnum.getDesc());
        return result;
    }
    
     /**
     * 返回通用成功
     * @return Result
     */
    public static Result ok(){
        return new Result().setResult(ResultCodeEnum.SUCCESS);
    }

    /**
     * 返回通用失败,未知错误
     * @return Result
     */
    public static Result error(){
        return new Result().setResult(ResultCodeEnum.UNKNOWN_ERROR);
    }
}

如上,整个返回结果类定义已经比较精简,通过 @Data 和 @Accessors(chain = true) 注解实现了get/set 方法和链式返回,并定义了通过枚举类创建对象的方法,并提供了直接返回的成功和失败方法。

结果类使用展示

java 复制代码
@PostMapping("get")
public String getInfo(){
    // 处理逻辑在这里
    String result = "返回结构或";
    
    // 封装返回结果,使用默认成功结果
    // return Result.ok().setData(result);
    
    // 封装返回结果,使用默认失败结果
    // return Result.error();
    
    // 封装返回结果,使用自定义枚举类
    return Result.setResult(ResultCodeEnum.NULL_POINT_ERROR);
}

最终篇:建造者模式有话说

进阶之后的返回结果类已经很简洁,并且使用也比较方便,已经是一个完整的结果类了,可以满足大部分场景下的使用。

但是,对于代码开发来讲,就是要不断优化我们的代码结构,使之无论从看起来、还是用起来、以及讲起来都要更加的合理且优雅,那么这个时候,设计模式就有话说了。

在进阶篇中,我们使用了结果枚举 + 链式返回,已经有了建造者模式的影子了,结果枚举就类似于建造者对象的简陋版,链式返回在建造者对象属性赋值中也有使用。

接下来看一下使用建造者模式来实现返回结果类的方法

建造者和结果对象,相亲相爱一家人

标准的建造者模式认为,需要定义抽象接口来定义建造者的行为,并实现类来与目标对象关联。

为了方便及展示其密切关联性,我们实现一个简化版的建造者模式,并将建造者对象作为结果对象的内部静态类实现。

java 复制代码
public class Result {
    private String code;
    private String desc;
    private Object data;

    private Result(ResultBuilder resultBuilder) {
        this.code = resultBuilder.code;
        this.desc = resultBuilder.desc;
        this.data = resultBuilder.data;
    }

    // 定义静态方法创建 ResultBuilder 类,否则使用时需要 new Result.ResultBuilder() 
    public static ResultBuilder builder(){
        return new ResultBuilder();
    }

    public static class ResultBuilder{
        private String code;
        private String desc;
        private T data;

        public ResultBuilder code(String code) {
            this.code = code;
            return this;
        }

        public ResultBuilder desc(String desc) {
            this.desc = desc;
            return this;
        }

        public ResultBuilder data(Object data) {
            this.data = data;
            return this;
        }

        public ResultBuilder resultCodeEnum(ResultCodeEnum resultCodeEnum){
            this.success = resultCodeEnum.getSuccess();
            this.code = resultCodeEnum.getCode();
            this.desc = resultCodeEnum.getDesc();
            return this;
        }

        public Result build(){
            Objects.requireNonNull(this.success);
            return new Result(this);
        }

        public Result successBuild(){
            return this.resultCodeEnum(ResultCodeEnum.SUCCESS).build();
        }
        public Result errorBuild(){
            return this.resultCodeEnum(ResultCodeEnum.UNKNOWN_ERROR).build();
        }
    }

}

使用建造者模式实现返回结果类,可以避免直接对返回结果类属性的修改,而是通过定义的建造者对象 builder 来赋值,保证了结果对象的数据安全。

内部静态建造者类使用

对于内部静态类创建时,需要携带其外部类名称才可以使用,如

java 复制代码
Result result = new Result.ResultBuilder().data("result").build();

为了实际使用方便,可以在外部类中定义静态方法进行 builder 对象的创建,即 builder() 方法

java 复制代码
// 使用时创建方法:Result.builder() 
public static ResultBuilder builder(){
    return new ResultBuilder();
}

此时创建方法可以写成

java 复制代码
Result result = Result.builder().data("result").build();

是不是很熟悉!在许多优秀的框架使用过程中,重要对象的创建方式就是类似上述的建造者链式创建方式。

lombok: 继续上分

对于建造者模式的实现,lombok 也提供了实现方案,可以通过 @Builder 注解为类实现内部静态的建造者类,与上述代码基本一致,展现代码可以更简洁。

java 复制代码
@Builder
public class Result {
    private String code;
    private String desc;
    private Object data;
}

太简单了有木有!

@Builder 注解实现的建造者模式是最基本的形式,使用时需要注意

  1. @Builder 注解只会为 Result 类定义全参数构造方法供 buidler 使用,没有无参构造,如果需要要自己实现或使用 @AllArgsConstructor 和 @NoArgsConstructor 注解
  2. 上述代码中没有使用 @Data 注解,Result 对象的属性不可修改,可以通过属性名称获取,如需要可以自行添加
  3. @Builder 注解实现的建造者模式虽然简单,但是太简单,无法使用我们进阶篇提到的枚举结果来实现返回对象,因此需要手动实现对应创建方法

实际使用过程中,可以根据需要选择或定义适合的返回结果类

接口数据格式一览

定义好返回结果枚举类和最终的返回结果类后,在 controller 控制器中创建一个接口并返回统一结果类信息

java 复制代码
@PostMapping("get")
public String getInfo(){
    // 处理逻辑在这里
    String result = "返回结果";
    
    // 封装返回结果
    return Result.builder().data(result).build();
}

通过 http 请求接口,可以得到如下格式的返回结果:

json 复制代码
{
  "code": 20000,
  "desc": "查询成功",
  "data": "返回结果";
}

这样,一个统一的结果返回类就创建成功了,在项目的开发过程中可以使用自定义的统一返回结果,如果使用了枚举类,只需要将返回结果枚举类维护起来,使用非常的方便哦。

最后

通过逐步的功能丰富,实现了一个满足基本使用需求的封装结果类,对项目开发过程会提供很大的帮助,提升编码效率并规范代码格式,并树立正确规范的代码观,希望每一位 coder 都能成长为参天大树,为行业添砖加瓦。

后续

结果类封装好了,但是却不想用怎么办?

相信很多人在开发中都有上述的想法,虽然你封装的很好,但是我就是不想用,这个时候我们就可以祭出 aop 神器,你不用我不用,成熟的代码自己动,感兴趣的小伙伴可以先学习起来哦。

相关推荐
uzong24 分钟前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天24 分钟前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享1 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪2 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
韩师傅3 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
栈与堆3 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥3 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似3 小时前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole