全局统一异常和统一返回演变历程

背景

其实在springmvc本身返回的就有一个对象专门来处理和返回结果统一返回:

java 复制代码
@PostMapping("/user/save")
@ApiOperation("保存用户")
public ResponseEntity<User> saveUser(@RequestBody  User user) {
    ResponseEntity<User> responseEntity = new ResponseEntity<>(user, HttpStatus.OK);
    return responseEntity;
}

在上面可以很清晰看到,springmvc提供了一个统一返回的类:ResponseEntity.java。而且这个类本身就是springmvc提供给浏览器返回的结果的影响的类。

但是这个类:ResponseEntity有一个弊端,就是它状态和提示信息我们无法进行扩展和更改。以及提示信息和状态和我们业务有时候根本没有任何联系。我们就只能借鉴它的这个思维,自己去扩展一个出来。来符合我业务的发展和需要。

1.普通类封装

java 复制代码
package com.pug.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class R {
    /**
     * 业务错误码
     */
    private Integer code;
    /**
     * 描述
     */
    private String msg;
    /**
     * 结果集
     */
    private Object data;

}

方法封装

java 复制代码
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) 

    R r = new R();
    r.setCode(200);
    r.setMsg("成功");
    r.setData(user);
    return r;
}

构造函数封装

java 复制代码
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) {
    return new R(200,"成功",user);
}

2.静态类封装

java 复制代码
package com.pug.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
public class R {
    /**
     * 业务错误码
     */
    private Integer code;
    /**
     * 描述
     */
    private String msg;
    /**
     * 结果集,需要返回的结果或数据
     */
    private Object data;


    public R(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


    /**
     * 成功方法的封装
     *
     * @param data
     * @return
     */
    public static R success(Object data) {
        return new R(200, "success", data);
    }
    /**
     * 失败方法的封装
     * 成功只有一种 200 成功
     * 失败就N中,也就code不同,原因不同,
     * @param code
     * @param msg
     * @return
     */
    public static R fail(Integer code,String msg) {
        return new R(code, msg, null);
    }

    /**
     *
     * @return
     */
    public R msg(String msg) {
        this.setMsg(msg);
        return this;
    }

    /**
     *
     * @return
     */
    public R code(Integer code) {
        this.setCode(code);
        return this;
    }
    
    private R(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

}

调用

java 复制代码
成功的调用
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) {
    return R.success(user).code(200).msg("成功");
}

最终采用构造函数调用使调用方式统一【在类的静态方式进行构造函数的实例化】

java 复制代码
构造函数私有化,外部不能在new 对象,让所有的调用都统一。即成功与失败都一样
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) {
    return new R(code,message,user);//code,message的参数问题没解决
}

解决code,message传参问题-# 提示信息的封装 - 常量类

java 复制代码
常量类的封装
package com.pug.common;

public interface Contants {

    Integer USER_INPUT_CODE = 210;
    String USER_INPUT_CODE_TEXT = "用户找不到";

}


调用
    @PostMapping("/user/save")
    @ApiOperation("保存用户")
    public R saveUser(@RequestBody User user) {
        if(user==null){
            return R.fail(Contants.USER_INPUT_CODE,Contants.USER_INPUT_CODE_TEXT);
        }
        return R.success(user).code(101).msg("成功");
    }

问题

  • 使用常量类,是没问题,但是很多时候,因为你取名字或者或者成双结对的方式放在一起都是一个封装困难的事情。
  • 不利于扩展,不具备面向对象的特征 (所有程序员都在维护这个文件,就造成大量冲突,名字冲突,状态冲突)
    • 解决方案:你们团队的状态自己建设一个接口或者常量类。

枚举的出现:

  • 面向对象的常量类。
  • 具有面向对象的特征
  • 枚举是一个类
  • 枚举可以实现接口
java 复制代码
package com.pug.common;
import lombok.Getter;
import org.checkerframework.checker.units.qual.A;

public enum AdminResultEnum {
    //枚举的构造函数私有化只能在内部实例化
    SERVER_SUCCESS(200, "success"),
    USER_INPUT_ERROR(201, "用户名找不到"),
    ORDER_INPUT_ERROR(202, "订单输入找不到");


    private Integer code;
    private String msg;

    AdminResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

R类改造

java 复制代码
package com.pug.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
public class R {
    /**
     * 业务错误码
     */
    private Integer code;
    /**
     * 描述
     */
    private String msg;
    /**
     * 结果集
     */
    private Object data;

    // 私有化的目的:不是单例,是为了约束外界不允许通过实例化和set方法的方式进行复制调用返回
    // 全部统一使用静态方法 success和fail进行调用。从而达到统一的目的
    private R(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


    /**
     * 成功方法的封装
     *
     * @param data
     * @return
     */
    public static R success(Object data) {
        return new R(AdminResultEnum.SERVER_SUCCESS.getCode(),
                AdminResultEnum.SERVER_SUCCESS.getMsg(), data);
    }

    /**
     * 成功方法的封装
     * 成功只有一种 200 成功
     * 失败就N中,也就code不同,原因不同,
     * @param code
     * @param msg
     * @return
     */
    public static R fail(Integer code,String msg) {
        return new R(code, msg, null);
    }
    此处调用方式与成功不同
    public static R fail(AdminResultEnum adminResultEnum) {
        return new R(adminResultEnum.getCode(), adminResultEnum.getMsg(), null);
    }
    /**
     *
     * @return
     */
    public R msg(String msg) {
        this.setMsg(msg);
        return this;
    }
    /**
     *
     * @return
     */
    public R code(Integer code) {
        this.setCode(code);
        return this;
    }

}

使用

java 复制代码
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) {
    if(user==null){
        return R.fail(AdminResultEnum.USER_INPUT_ERROR);
    }
    return R.success(user);
}

关于统一返回的提示信息的封装 - 枚举继承

上面确实可以简化很多操作,在实战开发中,如果你团队人数,不多只有3~11个人,或者业务模块不多,其实架构师和技术leader就可以把这些状态全部定义好或者定义完毕。

但是,如果你团队100~200 ,业务几十上百的业务和模块的时候,架构师或者技术leader不可能吧所有的状态都自己去定义,这个是不现实。

1:定义接口

java 复制代码
package com.pug.common;


public interface AdminResultInterface {

    Integer getCode();
    String getMsg();

}

2:用户状态枚举

java 复制代码
package com.pug.common;


public enum AdminUserResultEnum implements AdminResultInterface{

    USER_INPUT_ERROR(100601, "用户输入有误");


    private Integer code;
    private String msg;

    AdminUserResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}

3:订单状态枚举

java 复制代码
package com.pug.common;


public enum AdminOrderResultEnum implements AdminResultInterface{

    ORDER_INPUT_ERROR(200202, "订单输入找不到");

    private Integer code;
    private String msg;

    AdminOrderResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}

4:成功状态管理

java 复制代码
package com.pug.common;


public enum AdminSuccessResultEnum implements AdminResultInterface{

    SUCCESS_RESULT(200, "success");

    private Integer code;
    private String msg;

    AdminSuccessResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}

5:R类改造

java 复制代码
package com.pug.common;

import lombok.Data;


@Data
public class R {
    /**
     * 业务错误码
     */
    private Integer code;
    /**
     * 描述
     */
    private String msg;
    /**
     * 结果集
     */
    private Object data;

    // 私有化的目的:不是单列,是为了约束外界不允许通过实例化和set方法的方式进行复制调用返回
    // 全部统一使用静态方法 success和fail进行调用。从而达到统一的目的
    private R(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }


    /**
     * 成功方法的封装
     *
     * @param data
     * @return
     */
    public static R success(Object data) {
        return new R(AdminSuccessResultEnum.SUCCESS_RESULT.getCode(),
                AdminSuccessResultEnum.SUCCESS_RESULT.getMsg(), data);
    }

    public static R fail(AdminResultInterface adminResultEnum) {
        return new R(adminResultEnum.getCode(), adminResultEnum.getMsg(), null);
    }

    /**
     * 成功方法的封装
     * 成功只有一种 200 成功
     * 失败就N中,也就code不同,原因不同,
     * @param code
     * @param msg
     * @return
     */
    @Deprecated
    private static R fail(Integer code,String msg) {
        return new R(code, msg, null);
    }

    /**
     *
     * @return
     */
    public R msg(String msg) {
        this.setMsg(msg);
        return this;
    }

    /**
     *
     * @return
     */
    public R code(Integer code) {
        this.setCode(code);
        return this;
    }

}

6:使用

java 复制代码
@PostMapping("/user/save")
@ApiOperation("保存用户")
public R saveUser(@RequestBody User user) {
    if(user==null){
    //此处使用接口--多态的提现
        return R.fail(AdminUserResultEnum.USER_INPUT_ERROR);
    }
    return R.success(user);
}

springmvc的异常处理有如下解决方案

1: 自己try/catch

java 复制代码
@GetMapping("/user/{id}")
@ApiOperation("根据ID查询用户明细")
public R  getUser(@PathVariable("id") Long id) {
    try {
        log.trace("trace打印日志");
        log.debug("debug打印日志");
        String name = "zhangsan";
        log.info("info打印日志,{},{}", name, id);
        log.warn("warn打印日志");
        log.error("error打印日志");
        return R.success(userService.getById(id));
    }catch (Exception ex){
        ex.printStackTrace();
        return R.fail(AdminUserResultEnum.USER_SERVER_ERROR);
    }
}

2:抛出异常交给jvm处理

java 复制代码
@GetMapping("/user/{id}")
@ApiOperation("根据ID查询用户明细")
public R  getUser(@PathVariable("id") Long id) {
    try {
        log.trace("trace打印日志");
        log.debug("debug打印日志");
        String name = "zhangsan";
        log.info("info打印日志,{},{}", name, id);
        log.warn("warn打印日志");
        log.error("error打印日志");
        return R.success(userService.getById(id));
    }catch (Exception ex){
        throw new RuntimeException("服务器出现异常...");
    }
}

springmvc的统一异常处理捕获的好处

  • 集中管理和返回

告诉你道理:在springmvc的方法中,如果你已经做了统一异常处理。那么未来在springmvc方法你就放心的写代码。不需要去try/catch 1.controller

UserController.java 复制代码
package com.pug.zixun.contorller;


@RestController
@Slf4j
@Api(tags = "用户管理")
public class UserController extends BaseController {

    @Autowired
    private IUserService userService;

    @PostMapping("/user/save")
    @ApiOperation("保存用户")
    public R saveUser(@RequestBody User user) {
        if(user==null){
            throw new RuntimeException("此用户找不到....");
        }

        if(StringUtils.isEmpty(user.getUsername())){
            throw new RuntimeException("请输入用户名称....");
        }

        return R.success(user);
    }

    @GetMapping("/user/{id}")
    @ApiOperation("根据ID查询用户明细")
    public R  getUser(@PathVariable("id") Long id) {
        log.trace("trace打印日志");
        log.debug("debug打印日志");
        String name = "zhangsan";
        log.info("info打印日志,{},{}", name, id);
        log.warn("warn打印日志");
        log.error("error打印日志");
        return R.success(userService.getById(id));
    }
}

2.定义统一异常返回【与R相同职责不同】

java 复制代码
package com.pug.zixun.common.ex;


import com.pug.zixun.common.result.enums.AdminResultInterface;


public class ErrorHandler {
    // ErrorHandler === R 答案:不想破坏R类。
    // 异常的状态码,从枚举中获得
    private Integer status;
    // 异常的消息,写用户看得懂的异常,从枚举中得到
    private String message;
    // 异常的名字
    private String exception;


    public static ErrorHandler fail(Integer code,String message, Throwable throwable) {
        ErrorHandler errorHandler = new ErrorHandler();
        errorHandler.setMessage(message);
        errorHandler.setStatus(code);
        errorHandler.setException(throwable.getClass().getName());
        return errorHandler;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

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

    public String getException() {
        return exception;
    }

    public void setException(String exception) {
        this.exception = exception;
    }
}

3.定义全局异常捕获

java 复制代码
package com.pug.zixun.common.ex;

import com.pug.zixun.common.result.enums.AdminErrorResultEnum;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;


@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 对所有异常进行捕获
     * 缺点:不明确
     * Throwable
   */
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        // 1: 一定要加下面这行代码。打印异常堆栈信息,方便程序员去根据异常排查错误 --服务开发者
        e.printStackTrace();
        // 2: 出现异常,统一返回固定的状态---服务用户
        ErrorHandler errorHandler = ErrorHandler.fail(AdminErrorResultEnum.SERVER_ERROR.getCode(),
                AdminErrorResultEnum.SERVER_ERROR.getMsg(), e);
        // 3: 最后返回
        return errorHandler;
    }

    /**
     * 对所有异常进行捕获
     * 缺点:不明确
     * RuntimeException
     */
    @ExceptionHandler(RuntimeException.class)
    public ErrorHandler makeExcepton2(RuntimeException e, HttpServletRequest request) {
        // 1: 一定要加下面这行代码。打印异常堆栈信息,方便程序员去根据异常排查错误 --服务开发者
        e.printStackTrace();
        // 2: 出现异常,统一返回固定的状态---服务用户
        ErrorHandler errorHandler = ErrorHandler.fail(AdminErrorResultEnum.SERVER_ERROR.getCode(),e.getMessage(), e);
        // 3: 最后返回
        return errorHandler;
    }
    /**
     * 对所有异常进行捕获
     * 缺点:不明确
     * RuntimeException
     */
    @ExceptionHandler(PugBusinessException.class)
    public ErrorHandler makeExcepton3(PugBusinessException e, HttpServletRequest request) {
        // 1: 一定要加下面这行代码。打印异常堆栈信息,方便程序员去根据异常排查错误 --服务开发者
        e.printStackTrace();
        // 2: 出现异常,统一返回固定的状态---服务用户
        ErrorHandler errorHandler = ErrorHandler.fail(e.getCode(),e.getMessage(), e);
        // 3: 最后返回
        return errorHandler;
    }


    /**
     * 对所有异常进行捕获
     * 缺点:不明确
     * RuntimeException
     */
    @ExceptionHandler(PugValidatorException.class)
    public ErrorHandler makeExcepton4(PugValidatorException e, HttpServletRequest request) {
        // 1: 一定要加下面这行代码。打印异常堆栈信息,方便程序员去根据异常排查错误 --服务开发者
        e.printStackTrace();
        // 2: 出现异常,统一返回固定的状态---服务用户
        ErrorHandler errorHandler = ErrorHandler.fail(e.getCode(),e.getMessage(), e);
        // 3: 最后返回
        return errorHandler;
    }



    /**
     * 对所有异常进行捕获
     * 缺点:不明确
     * RuntimeException
     */
    @ExceptionHandler(PugOrderException.class)
    public ErrorHandler makeExcepton5(PugOrderException e, HttpServletRequest request) {
        // 1: 一定要加下面这行代码。打印异常堆栈信息,方便程序员去根据异常排查错误 --服务开发者
        e.printStackTrace();
        // 2: 出现异常,统一返回固定的状态---服务用户
        ErrorHandler errorHandler = ErrorHandler.fail(e.getCode(),e.getMessage(), e);
        // 3: 最后返回
        return errorHandler;
    }
}

全局统一返回

java 复制代码
package com.zl.springbootssm.config.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.springbootssm.config.results.ErrorHandler;
import com.zl.springbootssm.config.results.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


//@RestControllerAdvice(basePackages = "com.zl.springbootssm.controller")//swagger也是springmvc项目,防止被拦截
@RestControllerAdvice
@Slf4j
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {

    /**
     * 是否支持advice功能,true是支持 false是不支持
     *
     * @param methodParameter
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        log.info("4------------->ResultResponseHandler supports");
//        Integer integer = OpenFlagThreadLocal.get();
//        return !integer.equals(1);
        log.info(methodParameter.toString());
        return true;
    }

    // 参数o 代表其实就是springmvc的请求的方法的结果
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        log.info("5------------->ResultResponseHandler beforeBodyWrite");
        // 对请求的结果在这里统一返回和处理
        if (o instanceof ErrorHandler) {
            // 1、如果返回的结果是一个异常的结果,就把异常返回的结构数据倒腾到R.fail里面即可
            ErrorHandler errorHandler = (ErrorHandler) o;
            return R.fail(errorHandler.getStatus(), errorHandler.getMessage());
        } else if (o instanceof String) {
            try {
                // 2、因为springmvc数据转换器对String是有特殊处理 StringHttpMessageConverter
                ObjectMapper objectMapper = new ObjectMapper();
                R r = R.success(o);
                return objectMapper.writeValueAsString(r);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        // 否则把springmvc的方法的返回值重新包裹返回
        return R.success(o);
    }
}

全局统一异常和全局统一返回的整个链路示意图

相关推荐
过客猫202241 分钟前
使用 deepseek实现 go语言,读取文本文件的功能,要求支持 ascii,utf-8 等多种格式自适应
开发语言·后端·golang
刘立军1 小时前
本地大模型编程实战(20)用langgraph和智能体实现RAG(Retrieval Augmented Generation,检索增强生成)(4)
人工智能·后端·llm
jingwang-cs2 小时前
内外网文件传输 安全、可控、便捷的跨网数据传输方案
人工智能·后端·安全
Aska_Lv2 小时前
Java8-Stream流-实际业务常用api案例
后端
Biehmltym3 小时前
【SpringMVC】概述 SSM:Spring + SpringMVC + Mybats
java·后端·spring
qw9493 小时前
SpringMVC
java·后端
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS医疗报销系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
B站计算机毕业设计超人3 小时前
计算机毕业设计SpringBoot+Vue.jst房屋租赁系统(源码+LW文档+PPT+讲解)
vue.js·spring boot·后端·eclipse·intellij-idea·mybatis·课程设计
m0_748248654 小时前
SpringBoot整合easy-es
spring boot·后端·elasticsearch
一个热爱生活的普通人5 小时前
golang的切片(Slice)底层实现解析
后端·go