概述
一個規範、易懂和優雅,以及結構清晰且易於理解的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("
:-: | :-: | :-:
");
for (Error error : Error.values()) {
stringBuilder.append(String.format("%s | %s | %s", error.getCode(), error.getReason(), error.getDescription())).append("
");
}
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 序列化失敗 |
… | … | … |