java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java设计接口状态码

浅析Java如何优雅的设计接口状态码和异常

作者:赵侠客

HTTP协议里定义了一系列的状态码用来表明请求的状态,如常用的200表示请求正常,404表示请求的资源不存在,所以本文就来和大家讨论一下如何优雅的设计接口状态码和异常,感兴趣的可以了解下

一、前言

目前大多互联网应用后端输出数据协议都是使用HTTP协议+JSON数据格式,HTTP协议里定义了一系列的状态码用来表明请求的状态,如常用的200表示请求正常,404表示请求的资源不存在。由于这些状态数量是有限的,无法完整的表达我们业务中的各种状态,所以一般会在返回的JSON中增加业务状态码,如请求参数不对、用户状态禁用、用户名密码错误等。首先要搞清楚HTTP状态码和我们业务状态的关系, 我们看一个简单的HTTP协议报文:

GET http://localhost/test?id=2

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 11 Mar 2024 06:42:39 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": "OK",
  "message": "OK",
  "data": {
    "id": 2,
    "userName": "test3@8531.cn"
  }
}

上面是一个简单的返回JSON数据的GET请求,其中响应头中的HTTP/1.1 200表明HTTP状态码是200,响应Body中的"code": "OK",是我们业务里定义的状态码。

HTTP状态码

HTTP 状态码是由 HTTP 协议定义的,用于表示 Web 服务器对请求的响应状态,每一个状态码都有特定的含义。虽然开发者可以自定义 HTTP 状态码,但并不推荐这样做,因为这可能会引起混淆或者与将来的 HTTP 规范相冲突。HTTP 状态码的值是三位数字,其中第一位数字表示响应类别,目前有以下五个类别:

HTTP状态码有非常多的作用:

业务状态码

业务状态码是在 HTTP 状态码之上,由应用程序自身定义的,以反映特定业务逻辑的状态。这些状态码可以针对不同的操作不同的条件提供更详细更具体的信息,以便客户端能够更好地理解和处理业务流程,根据不同的状态码采取相应的处理措施。业务状态码的主要作用有:

HTTP状态码和业务状态码的关系

业务状态码应该是包含的HTTP状态码,在实际项目开发中很多开发者定义了很多业务状态码,但是所有接口请求都是返回http状态码为200,这是很不好的,应该是当HTTP状态码中能表达业务请求的状态时应该返回对应的HTTP状态码,HTTP状态码无法表达业务状态时才自定义业务状态码。我们参考《Google API Design Guide (谷歌API设计指南)中文版》看看大厂业务状态码是如何定义的:下面是一个表格,其中包含google.rpc.Code中定义的所有gRPC错误代码及其原因的简短说明:

HTTPRPC描述
200OK没有错误
400INVALID_ARGUMENT客户端指定了无效的参数。 检查错误消息和错误详细信息以获取更多信息。
400FAILED_PRECONDITION请求不能在当前系统状态下执行,例如删除非空目录。
400OUT_OF_RANGE客户端指定了无效的范围。
401UNAUTHENTICATED由于遗失,无效或过期的OAuth令牌而导致请求未通过身份验证。
403PERMISSION_DENIED客户端没有足够的权限。这可能是因为OAuth令牌没有正确的范围,客户端没有权限,或者客户端项目尚未启用API。
404NOT_FOUND找不到指定的资源,或者该请求被未公开的原因(例如白名单)拒绝。
409ABORTED并发冲突,例如读-修改-写冲突。
409ALREADY_EXISTS客户端尝试创建的资源已存在。
429RESOURCE_EXHAUSTED资源配额达到速率限制。 客户端应该查找google.rpc.QuotaFailure错误详细信息以获取更多信息。
499CANCELLED客户端取消请求
500DATA_LOSS不可恢复的数据丢失或数据损坏。 客户端应该向用户报告错误。
500UNKNOWN未知的服务器错误。 通常是服务器错误。
500INTERNAL内部服务错误。 通常是服务器错误。
501NOT_IMPLEMENTED服务器未实现该API方法。
503UNAVAILABLE暂停服务。通常是服务器已经关闭。
504DEADLINE_EXCEEDED已超过请求期限。如果重复发生,请考虑降低请求的复杂性。

从Google定义的RPC状态码可以看出业务状态码里很多都使用了HTTP状态码,这样通过监控HTTP状态码也可以反映出业务的某些状态。

如何设计一套优雅的状态码

"工欲善其事,必先利其器"、“磨刀不误砍柴工”,状态码的设计是非常基础的工作,在很多项目开发过程中刚开始时项目比较急也没有考虑统一状态码,等项目做好后发现船已经太大了,没法掉头了,很多错的东西就将错就错,这也增加了项目的后期维护成本,后面接手的人不了解代码历史也往往会吐槽前人代码写的垃圾。在项目开始时就应该将这些基础的东西规范好,这对后面的开发者来说用着也方便,项目也好维护。那么如何设计一套优雅的状态码呢?我觉得有以下几点:

二、设计步骤

总体设计思路

业务状态码统一使用code枚举返回,可读性强,返回格式如下:

HTTP/1.1 200 

{
  "code": "OK",
  "message": "OK",
  "data": {
    "id": 2,
    "userName": "test3@8531.cn"
  }
}

其中code 业务状态码主要分为三类:

异常设计

异常设计UML图

ApiException

public interface ApiException {
    String getCode();
    String getMessage();
}

HttpException

public class HttpException extends RuntimeException  implements ApiException{
    @Getter
    private final HttpStatus httpStatus;
    private final String message;
    public HttpException(HttpStatus apiCode) {
        this(apiCode, apiCode.getReasonPhrase());
    }
    public HttpException(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }
    @Override
    public String getCode() {
        return httpStatus.name();
    }
    @Override
    public String getMessage() {
        return message;
    }
}

CommException

public class CommException extends HttpException implements ApiException{
    private CommCodeEnum commCodeEnum;

    public CommException(CommCodeEnum commCodeEnum) {
        super(commCodeEnum.getHttpStatus());
        this.commCodeEnum=commCodeEnum;
    }

    public CommException(CommCodeEnum commCodeEnum, String message) {
        super(commCodeEnum.getHttpStatus(), message);
        this.commCodeEnum=commCodeEnum;
    }

    @Override
    public String getCode() {
        return commCodeEnum.getEnumName();
    }

    @Override
    public String getMessage() {
        return commCodeEnum.getName();
    }
}

UserException

public class UserException extends HttpException implements ApiException {
    private UserCodeEnum userCodeEnum;
    public UserException(UserCodeEnum userCodeEnum) {
        super(userCodeEnum.getHttpStatus());
        this.userCodeEnum=userCodeEnum;
    }
    public UserException(UserCodeEnum userCodeEnum, String message) {
        super(userCodeEnum.getHttpStatus(), message);
        this.userCodeEnum=userCodeEnum;
    }
    @Override
    public String getCode() {
        return userCodeEnum.getEnumName();
    }
    @Override
    public String getMessage() {
        return userCodeEnum.getName();
    }
}

OrderException

public class OrderException extends HttpException implements ApiException {
    private OrderCodeEnum oderCodeEnum;
    public OrderException(OrderCodeEnum oderCodeEnum) {
        super(oderCodeEnum.getHttpStatus());
        this.oderCodeEnum=oderCodeEnum;
    }
    public OrderException(OrderCodeEnum oderCodeEnum, String message) {
        super(oderCodeEnum.getHttpStatus(), message);
        this.oderCodeEnum=oderCodeEnum;
    }
    @Override
    public String getCode() {
        return oderCodeEnum.getEnumName();
    }
    @Override
    public String getMessage() {
        return oderCodeEnum.getName();
    }
}

异常枚举设计

BaseEnum

如何优雅的处理枚举可以参考我的另一篇文章《项目中如何优雅的使用枚举

public interface BaseEnum {
    int getCode();
    String getName();
    String getEnumName();
}

CommCodeEnum

public enum CommCodeEnum implements BaseEnum {
    INVALID_ARGUMENT(HttpStatus.OK,600, "参数错误"),
    ;
    //公共错误码6xx
    private int code;
    @Getter
    private HttpStatus httpStatus;
    private String name;
    CommCodeEnum(HttpStatus httpStatus, Integer code, String name) {
        this.httpStatus = httpStatus;
        this.code = code;
        this.name = name;
    }
    @Override
    public int getCode() {
        return this.code;
    }
    @Override
    public String getName() {
        return name;
    }
    @Override
    public String getEnumName() {
        return this.name();
    }
}

UserCodeEnum

在定义业务状态码时,需要注意的是有一个HttpStatus参数,如果我们觉得该业务出错了接口不应该返回HTTP 200,就可以设置成对应的HTTP状态码,如用户名不存在,可以理解为HTTP 状态码里的404资源不存在,这样我们就设置成USERNAME_NOT_EXIST(HttpStatus.NOT_FOUND,10002, "用户名不存在")这样做的好处是在监控HTTP状态码异常时也可以监控到业务出问题了。

public enum UserCodeEnum implements BaseEnum {
    USERNAME_EXIST(HttpStatus.OK,10001, "用户名已存在"),
    USERNAME_NOT_EXIST(HttpStatus.NOT_FOUND,10002, "用户名不存在"),
    USERNAME_DISABLE(HttpStatus.OK,10003, "用户被禁用"),
    ;
    @Getter
    private HttpStatus httpStatus;
    //用户模块错误码10xxx
    private int code;
    private String name;
}

OrderCodeEnum

public enum OrderCodeEnum implements BaseEnum {
    ORDER_CANCELLED(HttpStatus.OK,20001, "订阅已取消"),
    ORDER_TIMEOUT(HttpStatus.OK,20002, "订阅已超时"),

    ;
    //订单模块错误码20xxx
    private int code;
    @Getter
    private HttpStatus httpStatus;
    private String name;
}

三、统一接口返回

为了达到统一的接口数据返回格式,我们需要定义统一的接口返回类ApiResult,其它定义的code业务状态码,message提示消息和业务数据data参数。

统一接口返回包装

@NoArgsConstructor
@Data
public class ApiResult {
    private String code;
    private String message;
    private Object data;
    public ApiResult(BaseEnum apiCode, Map<String, Object> data) {
        this.code = apiCode.getEnumName();
        this.message = apiCode.getName();
        this.data = data;
    }
    public ApiResult(HttpStatus httpStatus, Object data) {
        this.code = httpStatus.name();
        this.message = httpStatus.getReasonPhrase();
        this.data = data;
    }
    public ApiResult(HttpStatus httpStatus,String message, Object data) {
        this.code = httpStatus.name();
        this.message = message;
        this.data = data;
    }
    public ApiResult(BaseEnum apiCode, String explanation, Map<String, Object> data) {
        this.code = apiCode.getEnumName();
        this.message = apiCode.getName() + (explanation != null ? "【" + explanation + "】" : "");
        this.data = data;
    }
}

公共Controller

为了方便Controller返回统一的数据格式,我可以定义BaseController,重载多种返回数据格式,业务Controller只需继承BaseController就可以直接调用 return renderOk(data)返回统一的接口数据格式。

public abstract class BaseController {
    @Resource
    protected HttpServletRequest httpRequest;

    protected ResponseEntity<ApiResult> renderOk() {
        return renderOk(null);
    }

    protected ResponseEntity<ApiResult> renderOk(Map<String, Object> data) {
        ApiResult apiResult = new ApiResult(HttpStatus.OK, data);
        return new ResponseEntity<>(apiResult, HttpStatus.OK);
    }

    protected ResponseEntity<ApiResult> renderOk(Object data) {
        ApiResult apiResult = new ApiResult(HttpStatus.OK, data);
        return new ResponseEntity<>(apiResult, HttpStatus.OK);
    }

    protected ResponseEntity<ApiResult> renderError(HttpStatus apiCode) {
        ApiResult apiResult = new ApiResult(apiCode, null);
        return new ResponseEntity<>(apiResult, HttpStatus.OK);
    }

    protected ResponseEntity<ApiResult> renderError(BaseEnum apiCode) {
        ApiResult apiResult = new ApiResult(apiCode, null);
        return new ResponseEntity<>(apiResult, HttpStatus.OK);
    }

    protected ResponseEntity<Map<String, Object>> renderError(HttpException httpException) {
        Map<String, Object> map = ImmutableMap.of("code", httpException.getCode(), "message", httpException.getMessage());
        return new ResponseEntity<>(map, httpException.getHttpStatus());
    }

    protected ResponseEntity<ApiResult> renderError(HttpException httpException, Object data) {
        ApiResult apiResult = new ApiResult(httpException.getHttpStatus(), data);
        return new ResponseEntity<>(apiResult, httpException.getHttpStatus());
    }

    protected ResponseEntity<Map<String, Object>> render(Map<String, Object> map) {
        return new ResponseEntity<>(map, HttpStatus.OK);
    }
}

四、统一异常拦截

针对代码中的非正常行业,可以统一使用抛自定义异常的方式,这样只需配置一个统一的异常拦截器,统一返回状态码。

@Slf4j
@ControllerAdvice
public class ErrorHandler extends BaseController {
    @ExceptionHandler(value = {HttpException.class})
    public ResponseEntity<Map<String, Object>>  httpException(HttpException ex) {
        log.error("{}", ex);
        return renderError(ex);
    }
}

五、测试

这样我们使用状态码就比较简单了,主要分为三种:

@RestController
public class UserController extends BaseController {
    @Resource
    private UserService userService;
    @GetMapping("/test")
    public ResponseEntity<ApiResult> getById(@RequestParam Long id) {
        //1.公共异常
        if (id == null) {
            throw new CommException(CommCodeEnum.INVALID_ARGUMENT);
        }
        //2.http协议异常
        User user = null;
        try {
            user = userService.selectById(id);
            int b = 1 / 0;
        } catch (Exception exception) {
            throw new HttpException(HttpStatus.GATEWAY_TIMEOUT);
        }
        //3.用户模块异常
        if (user == null) {
          // throw new UserException(UserCodeEnum.USERNAME_NOT_EXIST);
        }
        //4.订单模块异常
        try {
            //调用订单

        } catch (Exception e) {
            //throw new OrderException(OrderCodeEnum.ORDER_TIMEOUT, "请求订单超时");
        }
        return renderOk(user);
    }
}

正常返回状态码

接口正常返回统一使用code:OK,http状态码为200,

公共异常可以设置非200HTTP状态码

公共异常状态码

HTTP异常 HTTP状态码都是非200

http异常状态码

用户异常HTTP状态码可以是200也可以非200

用户异常状态码

订单异常HTTP状态码可以是200也可以非200

订单异常状态码

六、总结

本文介绍了HTTP状态码及业务状态码的区别和作用,提出并实现一种统一维护业务状态码和HTTP状态码的思路,该思路融合了HTTP状态码,规范了接口返回格式,统一的业务状态码,大大方便了在系统中使用异常和定义状态码。

到此这篇关于浅析Java如何优雅的设计接口状态码和异常的文章就介绍到这了,更多相关Java设计接口状态码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文