RESTful & “优雅的”API 响应结构设计
概述
一个规范、易懂和优雅,以及结构清晰且易于理解的API响应结构,完全可以省去许多无意义的沟通和文档。
预览
操作成功:
{ "status": true, "timestamp": 1525582485337 }
操作成功:返回数据
{ "status": true, "result": { "users": [ {"id": 1, "name": "name1"}, {"id": 2, "name": "name2"} ] }, "timestamp": 1525582485337 }
操作失败:
{ "status": false, "error": { "error_code": 5002, "error_reason": "illegal_argument_error", "error_description": "The String argument[1] must have length; it must not be null or empty" }, "timestamp": 1525582485337 }
实现
定义 JSONEntity
@Data @Accessors(chain = true) public class JSONEntity<Result extends Object> implements Serializable { public enum Error { UNKNOWN_ERROR(-1, "unknown_error", "未知错误"), SERVER_ERROR(5000, "server_error", "服务器内部异常"), BUSINESS_ERROR(5001, "business_error", "业务错误"), ILLEGAL_ARGUMENT_ERROR(5002, "illegal_argument_error", "参数错误"), JSON_SERIALIZATION_ERROR(5003, "json_serialization_error", "JSON序列化失败"), UNAUTHORIZED_ACCESS(5004, "unauthorized_access", "未经授权的访问"), SIGN_CHECK_ERROR(5005, "sign_check_error", "签名校验失败"), FEIGN_CALL_ERROR(5006, "feign_call_error", "远程调用失败"); private int code; private String reason; private String description; Error(int code, String reason, String description) { this.code = code; this.reason = reason; this.description = description; } public int getCode() { return code; } public Error setCode(int code) { this.code = code; return this; } public String getReason() { return reason; } public Error setReason(String reason) { this.reason = reason; return this; } public String getDescription() { return description; } public Error setDescription(String description) { this.description = description; return this; } public static String toMarkdownTable() { StringBuilder stringBuilder = new StringBuilder("error_code | error_reason | error_description"); stringBuilder.append("\n:-: | :-: | :-:\n"); for (Error error : Error.values()) { stringBuilder.append(String.format("%s | %s | %s", error.getCode(), error.getReason(), error.getDescription())).append("\n"); } return stringBuilder.toString(); } public static String toJsonArrayString() { SerializeConfig config = new SerializeConfig(); config.configEnumAsJavaBean(Error.class); return JSON.toJSONString(Error.values(), config); } } @JSONField(ordinal = 0) public boolean isStatus() { return null == this.error; } @JSONField(ordinal = 1) private Result result; @JSONField(ordinal = 2) private Map<String, Object> error; @JSONField(ordinal = 3) public long getTimestamp() { return System.currentTimeMillis(); } public static JSONEntity ok() { return JSONEntity.ok(null); } public static JSONEntity ok(Object result) { return new JSONEntity<Object>().setResult(result); } public static JSONEntity error() { return JSONEntity.error(Error.UNKNOWN_ERROR); } public static JSONEntity error(@NonNull String errorDescription) { return JSONEntity.error(Error.BUSINESS_ERROR, errorDescription); } public static JSONEntity error(@NonNull Throwable throwable) { Error error = Error.SERVER_ERROR; String throwMessage = throwable.getMessage(); StackTraceElement throwStackTrace = throwable.getStackTrace()[0]; String errorDescription = String.format("%s[%s]: %s#%s():%s", // null != throwMessage ? throwMessage : error.getDescription(), throwable.getClass().getTypeName(),// throwStackTrace.getClassName(), throwStackTrace.getMethodName(), throwStackTrace.getLineNumber()// ); return JSONEntity.error(error, errorDescription); } public static JSONEntity error(@NonNull Error error) { return JSONEntity.error(error, error.getDescription()); } public static JSONEntity error(@NonNull Error error, @NonNull String errorDescription) { return JSONEntity.error(error.getCode(), error.getReason(), errorDescription); } public static JSONEntity error(int errorCode, @NonNull String errorReason, @NonNull String errorDescription) { ImmutableMap<String, Object> errorMap = ImmutableMap.of(// "error_code", errorCode,// "error_reason", errorReason,// "error_description", errorDescription ); return new JSONEntity<Object>().setError(errorMap); } @Override public String toString() { return JSON.toJSONString(this, true); } public static JSONEntity convert(@NonNull String JsonEntityJsonString) { return JSON.parseObject(JsonEntityJsonString, JSONEntity.class); } }
定义 BusinessException
/** * 业务异常, 抛出后最终由 SpringMVC 拦截器统一处理为通用异常信息格式 JSON 并返回; */ @Data public class AngerCloudBusinessException extends AngerCloudRuntimeException { private static final JSONEntity.Error UNKNOWN_ERROR = JSONEntity.Error.UNKNOWN_ERROR; private int errorCode = UNKNOWN_ERROR.getCode(); private String errorReason = UNKNOWN_ERROR.getReason(); private String errorDescription = UNKNOWN_ERROR.getDescription(); /** * 服务器错误以及简单的错误堆栈消息 * @param cause */ public AngerCloudBusinessException(Throwable cause) { this(JSONEntity.Error.SERVER_ERROR, String.format("%s[%s]",// cause.getMessage(), cause.getClass().getSimpleName())); } /** * 业务错误(附带具体的错误消息) * @param errorDescription */ public AngerCloudBusinessException(String errorDescription) { this(JSONEntity.Error.BUSINESS_ERROR, errorDescription); } /** * 指定错误类型 * @param error */ public AngerCloudBusinessException(JSONEntity.Error error) { this(error, null); } /** * 指定错误类型以及自定义消息 * @param error * @param errorDescription */ public AngerCloudBusinessException(JSONEntity.Error error, String errorDescription) { setAttributes(error); if (null != errorDescription) { this.errorDescription = errorDescription; } } /** * 未知错误 */ public AngerCloudBusinessException() {} /** * 自定义错误消息 * @param errorCode * @param errorReason * @param errorDescription */ public AngerCloudBusinessException(int errorCode, String errorReason, String errorDescription) { setAttributes(errorCode, errorReason, errorDescription); } private void setAttributes(JSONEntity.Error error) { this.setAttributes(error.getCode(), error.getReason(), error.getDescription()); } private void setAttributes(int errorCode, String errorReason, String errorDescription) { this.errorCode = errorCode; this.errorReason = errorReason; this.errorDescription = errorDescription; } }
@ExceptionHandler: 异常拦截处理
@Slf4j @ResponseBody @ControllerAdvice public class AngerCloudHttpExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<JSONEntity> handlerException(Throwable throwable, HttpServletRequest request) { String info = String.format("HttpRequest -> <URI: %s, Method: %s, QueryString: %s, body: %s>, ", // request.getRequestURI(),// request.getMethod(),// request.getQueryString(),// WebUtil.getBody(request)); // 参数错误: HTTP 状态码 400 if (throwable instanceof IllegalArgumentException) { log.warn("{} handlerException-IllegalArgumentException: {}", info, ExceptionUtil.getMessage(throwable)); return ResponseEntity.badRequest().body(JSONEntity.error(JSONEntity.Error.ILLEGAL_ARGUMENT_ERROR, throwable.getMessage())); } // 业务错误: HTTP 状态码 200 if (throwable instanceof AngerCloudBusinessException) { AngerCloudBusinessException exception = (AngerCloudBusinessException) throwable; if (JSONEntity.Error.SERVER_ERROR.getCode() != exception.getErrorCode() // && JSONEntity.Error.UNKNOWN_ERROR.getCode() != exception.getErrorCode()) { log.warn("{} handlerException-BusinessError: {}", info, exception.getErrorDescription()); return ResponseEntity.ok(JSONEntity.error(exception.getErrorCode(), exception.getErrorReason(), exception.getErrorDescription())); } } // 系统错误: HTTP 状态码 500 log.error(StrUtil.format("{} handlerException-Exception: {}", info, throwable.getMessage()), throwable); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(JSONEntity.error(throwable)); } }
关于 httpStatus 的设定,建议如下:
error | error_reason | http_status |
---|---|---|
illegal_argument_error | 参数错误 | 400 |
unknown_error | 未知错误 | 500 |
server_error | 服务器内部异常 | 500 |
xxx | 其他 | 200 |
使用
JSONEntity:
@GetMapping("/user/{id}") public JSONEntity<User> user(@PathVariable Integer id) { User user = find(id); if (null == user) { return JSONEntity.error(JSONEntity.Error.USER_NOT_FOUND); } return JSONEntity.ok(ImmutableMap.of("user": user)); } --> { "status": true, "result": { "user": {"id": 1, "name": "user1"} }, "timestamp": 1525582485337 } { "status": false, "error": { "error_code": 10086, "error_reason": "user_not_found", "error_description": "没有找到用户 #user1" }, "timestamp": 1525582485337 }
Assert: 参数检查
Assert.noEmpty(name, "用户名不能为空"); // 或者手动抛出IllegalArgumentException异常 if (StringUtil.isEmpty(name)) { throw new IllegalArgumentException("用户名不能为空"); } --> { "status": false, "error": { "error_code": 5002, "error_reason": "illegal_argument_error", "error_description": "用户名不能为空" }, "timestamp": 1525582485337 }
Exception: 抛出业务异常
// 使用系统定义的错误 throw new AngerCloudBusinessException(JSONEntity.Error.USER_NOT_FOUND) // 指定系统定义的错误, 错误消息使用异常信息中携带的消息 throw new AngerCloudBusinessException(JSONEntity.Error.USER_NOT_FOUND, ex); // 指定系统定义的错误, 但指定了新的错误消息. throw new AngerCloudBusinessException(JSONEntity.Error.USER_NOT_FOUND, "用户 XXX 没有找到"); // 手动抛出服务器异常 (SERVER_ERROR) throw new AngerCloudBusinessException(ex); // 抛出一个未知的异常 (UNKNOWN_ERROR) throw new AngerCloudBusinessException(); // 自定义错误消息 thow new AngerCloudBusinessException(10086, "user_email_exists", "用户邮箱已经存在了"); { "status": false, "error": { "error_code": 10086, "error_reason": "user_email_exists", "error_description": "用户邮箱已经存在了" }, "timestamp": 1525582485337 }
补充:提供可维护的 ErrorCode 列表
@GetMapping("/error_code") public JSONEntity<?> errors() { return JSONEntity.ok(ImmutableMap.of(// "enums", JSONEntity.Error.values(),// "markdownText", JSONEntity.Error.toMarkdownTable(), // "jsonString", JSONEntity.Error.toJsonArrayString())); }
最终,该接口会根据系统定义(JSONEntity.Error)的异常信息,返回 Markdown 文本或 JSON 字符串,前端解析后生成表格即可,就像下面这样:
error_code | error_reason | error_description |
---|---|---|
-1 | unknown_error | 未知错误 |
5000 | server_error | 服务器内部异常 |
5001 | illegal_argument_error | 参数错误 |
5002 | json_serialization_error | JSON 序列化失败 |
... | ... | ... |
相关推荐
Eiceblue 2020-08-02
ahnjwj 2020-07-28
playis 2020-06-28
playis 2020-06-16
ahnjwj 2020-06-12
84560296 2020-06-10
84560296 2020-06-09
84560296 2020-06-08
84560296 2020-05-30
81901836 2020-05-26
beibeijia 2020-05-16
85291545 2020-05-01
84560296 2020-04-10
fanix 2020-04-09
bapinggaitianli 2020-04-07
84560296 2020-03-27
85291545 2020-03-26
82911731 2020-03-25