TABLE OF CONTENTS

  1. 设计目标
  2. 实现思路
    1. 统一响应格式
    2. 统一异常处理
  3. 响应示例
  4. 使用
  5. 核心代码实现
  6. 思考
    1. 响应码映射
    2. HTTP状态码
  7. 总结
  8. 参考

写一个Spring项目统一响应格式设计和全局异常处理的best-practice,便于后续查阅。

设计目标

现在一个良好的web项目,基本采用前后端分离的格式,本次设计目标:

  • 统一响应格式,可以极大方便前后端研发之间的沟通,简化和统一双方的代码处理。
  • 全局统一异常处理的好处是消除分散在代码逻辑中异常处理,if分支和try-catch代码块。
  • 考虑响应码描述的国际化处理,参数替换,支持灵活的配置。
  • 调用方API层级的报错给出友好、结构化的提示。
  • 考虑服务端后台依赖的三方业务返回的响应码映射。
  • 研发使用起来简单,不易犯错。

实现思路

统一响应格式

结合工作经验和借鉴大厂的API定义,设计如下几个类:

  • BaseResult:基本响应基类,只包含结果码code, 结果码描述msg,一般地所有的响应子类继承该类型。
  • Response:统一响应类,继承自BaseResult(必选),自身包含业务数据data(必选,没有内容时为null),额外数据payload(可选),API层级报错列表errors(可选),Response通过建造者模式构建。
  • ResponseCodeInterface:响应码接口,提供三个方法code(), message(), i18nKey(),所有响应码枚举类均实现该接口并重写三个方法,确保新增响应码枚举类型时提供统一的访问入口,面向接口编程。
  • ResponseEnum:响应码集合。自身为全局结果码集合,包含所有类型(全局,Internal, External, Business, Client)的响应码列表,三个字段分别为响应码code, 默认响应描述(允许带占位符)msg,响应码描述的国际化配置键i18nKey

统一异常处理

将所有异常处理抽出,放到@ControllerAdvice类中的@ExceptionHandler注解的方法中处理,针对不同异常执行不同的处理逻辑。

代码逻辑可能根据需要会抛出以下异常

  • 业务异常:ServiceException继承自RuntimeException,业务处理过程中,可以抛出ServiceException退出接下来的业务处理进入到统一异常处理,返回响应给客户端。
  • 受检异常:业务自身捕获处理,可以封装成ServiceException抛出。
  • 除业务异常外的非受检异常:仅处理Hiberante validator等可识别的异常,封装到List列表中。
  • 其他未知异常:Throwable统一设置业务内部异常。

附Java异常体系,忘记时可以查阅。

国际化:

  • 封装Spring I18N的messageSource到MessageUtils,提供国际化处理方法。
  • 针对API层级的报错,通过Hibernate Validator组件实现国际化。
  • 异常流程:创建ServiceException时执行错误描述信息国际化。
  • 正常处理:Response.ResponseBuilder构造方法执行结果码描述国际化。

响应示例

code和message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 正常响应1:
{
"code": 0,
"msg": "Success",
"data": {
"orderId": "ALIPAY0001000000000001",
"status": "COMPLETED",
"amount": "100.00"
},
"payload": {
"external_ext": {
"id": "10001",
"name": "test"
}
}
}

// 正常响应2

{
"code":0,
"msg":"Success",
"data":null
}

// 报错响应1

{
"code":4001,
"msg":"Failed",
"data":null,
"errors":[{
"name":"bankNo",
"message":"银行卡号不符合规范"
}
]
}

使用

结果码枚举增删改:ResponseEnum负责管理,所有返回给调用方的响应码均要在这里定义。

  • 业务流程处理成功:
    • Controller层可以直接返回Response.success(data)等封装好的常用响应,也可以通过Response.ResponseBuilder建造者生成响应返回。
  • 业务流程处理失败:
    • 直接创建并抛出ServiceException,并把需要传递的数据放入ServiceException的
    • 非主动抛出的异常,交由@ControllerAdvice处理

扩展点:

  • 如果需要新增异常类型和处理,可以继承ServiceException,然后在@ControllerAdvice中新增一个@ExceptionHandler注解的方法处理即可,适合细分不同业务子异常处理
  • 如果需要在返回响应前做一些公共的操作,可以实现ResponseBodyAdvice接口或者继承AbstractMappingJacksonResponseBodyAdvice抽象类来达成。
  • 对于数据库异常,可以通过AOP切面针对dao层进行处理
  • Filter过滤链的异常假如也希望捕获并处理,可以通过实现Filter接口重写doFilter,在catchException中捕获后封装成统一响应格式。
  • HandlerExceptionResolver接口 ===> Spring Web MVC

核心代码实现

ResponseCodeInterface接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface ResponseCodeInterface {

/**
* 返回响应码
* @return 响应码
*/
int code();

/**
* 返回具体的详细错误描述(非国际化信息,可能含有占位符)
* @return 响应码描述
*/
String message();

/**
* i18n资源文件的key,value为国际化响应码描述, 详见messages.properties文件
* @return key
*/
String i18nKey();
}

ResponseCodeEnum响应码集合枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
public enum ResponseCodeEnum implements ResponseCodeInterface {

/**
* 错误码集合
* ******以下是旧的设计****
* 1~9999 为保留错误码 或者 常用错误码
* 10000~19999 为内部错误码
* 20000~29999 客户端错误码 (客户端异常调用之类的错误)
* 30000~39999 为第三方错误码 (代码正常,但是第三方异常)
* 40000~49999 为业务逻辑 错误码 (无异常,代码正常流转,并返回提示给用户)
* 由于系统内的错误码都是独一无二的,所以错误码应该放在common包集中管理
* ---------------------------
* 错误码不一定位数一定要相同。比如腾讯的微信接口错误码的位数就并不相同。按照常理错误码的数量大小应该是:
* 内部错误码 < 客户端错误码 < 第三方错误码 < 业务错误码
* 所以我们应该尽可能的把错误码的数量留给业务错误码
* ---------------------------
* *******新的设计**********
* 1~99 为内部错误码(框架本身的错误)
* 100~999 客户端错误码 (客户端异常调用之类的错误)
* 1000~9999为第三方错误码 (代码正常,但是第三方异常)
* 10000~99999 为业务逻辑 错误码 (无异常,代码正常流转,并返回提示给用户)
* 由于系统内的错误码都是独一无二的,所以错误码应该放在common包集中管理
* ---------------------------
* 总体设计就是值越小 错误严重性越高
* 目前10000~19999是初始系统内嵌功能使用的错误码,后续开发者可以直接使用20000以上的错误码作为业务错误码
*/

SUCCESS(0, "Success", "COMMON.SUCCESS"),
PENDING(1, "In-progress", "COMMON.PENDING"),
FAILED(99999, "Failed", "COMMON.FAILED");

private final int code;
private final String message;
// 国际化描述的key,value为错误描述(可能含有占位符,占位符由程序上下文传入参数填充)
private final String i18nKey;

ResponseCodeEnum(int code, String message, String i18nKey) {
this.code = code;
this.message = message;
this.i18nKey = i18nKey;
}

@Override
public int code() {
return this.code;
}

@Override
public String message() {
return this.message;
}

@Override
public String i18nKey() {
return this.i18nKey;
}

/**
* 10000~99999 为业务逻辑 错误码 (无代码异常,代码正常流转,并返回提示给用户)
* 1XX01 XX是代表模块的意思 比如10101 01是Permission模块
* 错误码的命名最好以模块为开头 比如 NOT_ALLOWED_TO_OPERATE前面加上PERMISSION = PERMISSION_NOT_ALLOWED_TO_OPERATE
*/
public enum Business implements ResponseCodeInterface {

// ----------------------------- COMMON --------------------------------------

COMMON_OBJECT_NOT_FOUND(10001, "找不到ID为 {} 的 {}", "Business.OBJECT_NOT_FOUND"),

COMMON_UNSUPPORTED_OPERATION(10002, "不支持的操作", "Business.UNSUPPORTED_OPERATION"),

COMMON_FILE_NOT_ALLOWED_TO_DOWNLOAD(10003, "文件名称({})非法,不允许下载", "Business.FILE_NOT_ALLOWED_TO_DOWNLOAD"),

// ----------------------------- PERMISSION -----------------------------------

PERMISSION_FORBIDDEN_TO_MODIFY_ADMIN(10101, "不允许修改管理员的信息", "Business.FORBIDDEN_TO_MODIFY_ADMIN"),

PERMISSION_NOT_ALLOWED_TO_OPERATE(10202, "没有权限进行此操作,请联系管理员", "Business.NO_PERMISSION_TO_OPERATE"),

// ----------------------------- LOGIN -----------------------------------------

LOGIN_WRONG_USER_PASSWORD(10201, "用户密码错误,请重新输入", "Business.LOGIN_WRONG_USER_PASSWORD"),

LOGIN_ERROR(10202, "登录失败:{}", "Business.LOGIN_ERROR"),

LOGIN_CAPTCHA_CODE_WRONG(10203, "验证码错误", "Business.LOGIN_CAPTCHA_CODE_WRONG"),

LOGIN_CAPTCHA_CODE_EXPIRE(10204, "验证码过期", "Business.LOGIN_CAPTCHA_CODE_EXPIRE"),

LOGIN_CAPTCHA_CODE_NULL(10205, "验证码为空", "Business.LOGIN_CAPTCHA_CODE_NULL"),

// ----------------------------- UPLOAD -----------------------------------------

UPLOAD_FILE_TYPE_NOT_ALLOWED(10401, "不允许上传的文件类型,仅允许:{}", "Business.UPLOAD_FILE_TYPE_NOT_ALLOWED"),

UPLOAD_FILE_NAME_EXCEED_MAX_LENGTH(10402, "文件名长度超过:{} ", "Business.UPLOAD_FILE_NAME_EXCEED_MAX_LENGTH"),

UPLOAD_FILE_SIZE_EXCEED_MAX_SIZE(10403, "文件名大小超过:{} MB", "Business.UPLOAD_FILE_SIZE_EXCEED_MAX_SIZE"),

UPLOAD_IMPORT_EXCEL_FAILED(10404, "导入excel失败:{}", "Business.UPLOAD_IMPORT_EXCEL_FAILED"),

UPLOAD_FILE_IS_EMPTY(10405, "上传文件为空", "Business.UPLOAD_FILE_IS_EMPTY"),

UPLOAD_FILE_FAILED(10406, "上传文件失败:{}", "Business.UPLOAD_FILE_FAILED"),

// ---------------------------------- USER -----------------------------------------------

USER_NON_EXIST(10501, "登录用户:{} 不存在", "Business.USER_NON_EXIST"),

;


private final int code;
private final String message;
private final String i18nKey;

Business(int code, String message, String i18nKey) {
Assert.isTrue(code > 10000 && code < 99999,
"错误码code值定义失败,Business错误码code值范围在10000~99099之间,请查看ErrorCode.Business类,当前错误码码为" + name());

String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.message = message;
this.i18nKey = i18nKey;
}

@Override
public int code() {
return this.code;
}

@Override
public String message() {
return this.message;
}

@Override
public String i18nKey() {
return i18nKey;
}
}


/**
* 1000~9999是外部错误码 比如调用支付失败
* 需要确认是否要透传三方错误给客户端
*/
public enum External implements ResponseCodeInterface {

/**
* 支付宝调用失败
*/
FAIL_TO_PAY_ON_ALIPAY(1001, "支付宝调用失败", "External.FAIL_TO_PAY_ON_ALIPAY");


private final int code;
private final String message;

private final String i18nKey;

External(int code, String message, String i18nKey) {
Assert.isTrue(code > 1000 && code < 9999,
"错误码code值定义失败,External错误码code值范围在1000~9999之间,请查看ErrorCode.External类,当前错误码码为" + name());

String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.message = message;
this.i18nKey = i18nKey;
}

@Override
public int code() {
return this.code;
}

@Override
public String message() {
return this.message;
}

@Override
public String i18nKey() {
return this.i18nKey;
}


}


/**
* 100~999是客户端错误码
* 客户端如 Web+小程序+手机端 调用出错
* 可能由于参数问题或者授权问题或者调用过去频繁
*/
public enum Client implements ResponseCodeInterface {

COMMON_FORBIDDEN_TO_CALL(101, "禁止调用", "Client.COMMON_FORBIDDEN_TO_CALL"),

COMMON_REQUEST_TOO_OFTEN(102, "调用太过频繁", "Client.COMMON_REQUEST_TOO_OFTEN"),

COMMON_REQUEST_PARAMETERS_INVALID(103, "请求参数异常,{0}", "Client.COMMON_REQUEST_PARAMETERS_INVALID"),

COMMON_REQUEST_METHOD_INVALID(104, "请求方式: {} 不支持", "Client.COMMON_REQUEST_METHOD_INVALID"),

COMMON_HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR(109, "请求类型:{} 不支持", "Client.COMMON_HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR"),

COMMON_REQUEST_RESUBMIT(105, "请求重复提交", "Client.COMMON_REQUEST_RESUBMIT"),

COMMON_NO_AUTHORIZATION(106, "请求接口:{} 失败,用户未授权", "Client.COMMON_NO_AUTHORIZATION"),

INVALID_TOKEN(107, "token异常", "Client.INVALID_TOKEN"),

TOKEN_PROCESS_FAILED(108, "token处理失败:{}", "Client.TOKEN_PROCESS_FAILED"),

;

private final int code;
private final String message;
private final String i18nKey;

Client(int code, String message, String i18nKey) {
Assert.isTrue(code > 100 && code < 999,
"错误码code值定义失败,Client错误码code值范围在100~999之间,请查看ErrorCode.Client类,当前错误码码为" + name());

String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.message = message;
this.i18nKey = i18nKey;
}

@Override
public int code() {
return this.code;
}

@Override
public String message() {
return this.message;
}

@Override
public String i18nKey() {
return this.i18nKey;
}

}


/**
* 0~99是内部错误码 例如 框架内部问题之类的
*/
public enum Internal implements ResponseCodeInterface {
/**
* 内部错误码
*/
INVALID_PARAMETER(1, "参数异常:{}", "Internal.INVALID_PARAMETER"),

/**
* 该错误主要用于返回 未知的异常(大部分是RuntimeException) 程序未能捕获 未能预料的错误
*/
INTERNAL_ERROR(2, "系统内部错误:{}", "Internal.INTERNAL_ERROR"),

GET_ENUM_FAILED(3, "获取枚举类型失败, 枚举类:{}", "Internal.GET_ENUM_FAILED"),

GET_CACHE_FAILED(4, "获取缓存失败", "Internal.GET_CACHE_FAILED"),

DB_INTERNAL_ERROR(5, "数据库异常", "Internal.DB_INTERNAL_ERROR"),

LOGIN_CAPTCHA_GENERATE_FAIL(7, "验证码生成失败", "Internal.LOGIN_CAPTCHA_GENERATE_FAIL"),

EXCEL_PROCESS_ERROR(8, "excel处理失败:{}", "Internal.EXCEL_PROCESS_ERROR"),

;

private final int code;
private final String message;

private final String i18nKey;

Internal(int code, String message, String i18nKey) {
Assert.isTrue(code < 100,
"错误码code值定义失败,Internal错误码code值范围在100~999之间,请查看ErrorCode.Internal类,当前错误码码为" + name());

String errorTypeName = this.getClass().getSimpleName();
Assert.isTrue(i18nKey != null && i18nKey.startsWith(errorTypeName),
String.format("错误码i18nKey值定义失败,%s错误码i18nKey值必须以%s开头,当前错误码为%s", errorTypeName, errorTypeName, name()));
this.code = code;
this.message = message;
this.i18nKey = i18nKey;
}

@Override
public int code() {
return this.code;
}

@Override
public String message() {
return this.message;
}

@Override
public String i18nKey() {
return this.i18nKey;
}

}

}

业务异常基类ServiceException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
@EqualsAndHashCode(callSuper = true)
@Data
@Slf4j
@ApiModel(value = "服务基本异常类型", description = "服务基本异常类型")
public class ServiceException extends RuntimeException {

/**
* 业务异常的结果码枚举
*/
@Getter
protected ResponseCodeInterface responseCode;

/**
* 下面2个message,在ServiceException实例化后,都表示替换了占位符的错误描述,一般地,通过getMessage()方法获取最终的错误描述
*/
protected String message;
protected String i18nMessage;

/**
* 额外数据,支持扩展
* <p>
* 如果某个接口由一些额外的数据,有一些特殊的数据 可以放在这个payload里面
*/
protected Map<String, Object> payload;

/**
* code => 传入的枚举类的code
* message => 传入的枚举类的message,不涉及参数替换,通常为一段无需替换占位符的错误描述
* i18nMessage => 传入枚举类的i18nKey字段的国际化信息,在对应的message_${language}_${country}.properties中查找该key对应的value
*
* @param responseCode
*/
public ServiceException(ResponseCodeInterface responseCode) {
fillPayload(null);
fillResponseCode(responseCode);
}

/**
* code => 传入的枚举类的code
* message => 替换了参数后的message,枚举类中该字段含有占位符的错误描述
* i18nMessage => 替换了参数后的i18nMessage,i18nKey字段的国际化信息含有占位符(也可以不含),会被args参数列表进行替换
*
* @param responseCode
* @param args
*/
public ServiceException(ResponseCodeInterface responseCode, Object... args) {
fillPayload(null);
fillResponseCode(responseCode, args);
}

/**
* 通常用于包装其他异常为ServiceException的场景
* 注意:如果是try catch的情况下捕获异常并转为ServiceException的话,一定要填入Throwable e,不然会丢失堆栈信息
* <p>
* code => 传入的枚举类的code
* message => 替换了参数后的message,枚举类中该字段含有占位符的错误描述
* i18nMessage => 替换了参数后的i18nMessage,i18nKey字段的国际化信息含有占位符(也可以不含),会被args参数列表进行替换
*
* @param e 捕获到的原始异常
* @param responseCode 错误码
* @param args 错误详细信息参数
*/
public ServiceException(ResponseCodeInterface responseCode, Throwable e, Object... args) {
super(e);
fillPayload(null);
fillResponseCode(responseCode, args);
}

public ServiceException(ResponseCodeInterface responseCode, Map<String, Object> payload, Object... args) {
fillPayload(payload);
fillResponseCode(responseCode, args);
}

public ServiceException(ResponseCodeInterface responseCode, Throwable e, Map<String, Object> payload, Object... args) {
super(e);
fillPayload(payload);
fillResponseCode(responseCode, args);
}

private void fillPayload(Map<String, Object> payload) {
this.payload = payload;
}

private void fillResponseCode(ResponseCodeInterface responseCode, Object... args) {
this.responseCode = responseCode;
// 替换占位符后的默认错误描述
this.message = MessageFormatter.arrayFormat(responseCode.message(), args).getMessage();

try {
// 替换占位符后的国际化错误描述
this.i18nMessage = MessageUtils.message(responseCode.i18nKey(), args);
} catch (BeansException e) {
log.error("MessageUtils not ready, depends on messageSource", e);
} catch (Exception e) {
log.error("could not found i18nMessage entry for key: " + responseCode.i18nKey(), e);
}
}

// 下面2个方法均来自Throwable,写法都一样,意味着ServiceException对象返回的都是经过参数替换后的国际化错误描述信息(无占位符可不替换,但如有配置国际化信息仍然展示国际化信息)
@Override
public String getMessage() {
return i18nMessage != null ? i18nMessage : message;
}

@Override
public String getLocalizedMessage() {
return i18nMessage != null ? i18nMessage : message;
}
}

全局统一异常处理ExceptionHandlerAdvice类:通过@RestControllerAdvice和@ExceptionHandler实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

/**
* 权限校验异常
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Response<?> handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
log.error(MessageFormat.format("请求地址 [{0}], 权限校验失败 [{1}]", request.getRequestURI(), e.getMessage()), e);
return Response.fail(new ServiceException(ResponseCodeEnum.Business.PERMISSION_NOT_ALLOWED_TO_OPERATE));
}

/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Response<?> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {

log.error(MessageFormat.format("请求地址 [{0}], 不支持 [{1}] 请求", request.getRequestURI(), e.getMethod()), e);
return Response.fail(new ServiceException(ResponseCodeEnum.Client.COMMON_REQUEST_METHOD_INVALID, e.getMethod()));
}


/**
* 捕获缓存类当中的错误
* <p>
* 需要确认是否要进行捕获
*/
@ExceptionHandler(UncheckedExecutionException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response<?> handleServiceException(UncheckedExecutionException e) {
log.error("获取Guava缓存出现异常!", e);
return Response.fail(new ServiceException(ResponseCodeEnum.Internal.GET_CACHE_FAILED));
}


/**
* 业务异常(相关错误码枚举、数据等已经完成封装)
* <p>
* 由开发者主动抛出该异常,因此异常e包含了ErrorCodeInterface对象(结果码code,结果描述message,国际化描述信息i18nMessage)
*/
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response<?> handleServiceException(ServiceException e, HttpServletRequest request) throws BindException {
log.warn("请求发生了预期异常,出错的 url [{}],出错的描述为 [{}]",
request.getRequestURL().toString(), e.getMessage());
log.warn("发生了业务异常:", e);

return Response.fail(e);
}

/**
* BindException、MethodArgumentNotValidException异常处理
*/
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<?> handleBindException(BindException e, HttpServletRequest request) {
log.error(MessageFormat.format("请求发生了参数校验异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);

Response.ResponseBuilder builder = new Response.ResponseBuilder(ResponseCodeEnum.Client.COMMON_REQUEST_PARAMETERS_INVALID);

// 对象级别的验证错误,自定义注解校验一个类多个字段的场景
for (ObjectError objectError : e.getGlobalErrors()) {
builder.addError(new Response.Error(objectError.getObjectName(), objectError.getDefaultMessage()));
}

// 字段级别的验证错误
for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
builder.addError(new Response.Error(fieldError.getField(), fieldError.getDefaultMessage()));
}

return builder.build();
}


// ---------------------------方式二-------------------------

/**
* 针对参数校验失败异常的处理
*
* @param e 参数校验异常
* @param request http request
* @return 异常处理结果
*/
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<?> handleDatabindException(ConstraintViolationException e, HttpServletRequest request) {
log.error(MessageFormat.format("请求发生了非预期异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);

List<Response.Error> errorList = new ArrayList<>();
for (ConstraintViolation cv : e.getConstraintViolations()) {
errorList.add(new Response.Error(cv.getPropertyPath().toString(), cv.getMessage()));
}

return new Response.ResponseBuilder<>(ResponseCodeEnum.Client.COMMON_REQUEST_PARAMETERS_INVALID, JsonUtil.toJson(errorList))
.errors(errorList)
.build();

}

/**
* 针对spring web 中的异常的处理
*
* @param e Spring Web 异常
* @param request http request
* @param response http response
* @return 异常处理结果
*/
@ExceptionHandler(value = {
NoHandlerFoundException.class
})
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response<?> handleSpringWebExceptionHandler(Exception e, HttpServletRequest request, HttpServletResponse response) {
log.error(MessageFormat.format("请求发生了非预期异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);

return new Response.ResponseBuilder<>(ResponseCodeEnum.Client.COMMON_REQUEST_METHOD_INVALID).build();
}

/**
* HttpMediaTypeNotSupportedException异常
*
* @param e
* @param request
* @return
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Response<?> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e, HttpServletRequest request) {
log.error(MessageFormat.format("请求发生了非预期异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);
return Response.fail(new ServiceException(ResponseCodeEnum.Client.COMMON_HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR));
}

@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response<?> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.error(MessageFormat.format("请求发生了运行时异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);

return new Response.ResponseBuilder<>(ResponseCodeEnum.Internal.INTERNAL_ERROR).build();
}

/**
* 针对全局异常的处理
*
* @param e 全局异常
* @param request http request
* @param response http response
* @return 异常处理结果
*/
@ExceptionHandler(value = Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response<?> throwableHandler(Throwable e, HttpServletRequest request, HttpServletResponse response) {
log.error(MessageFormat.format("请求发生了非预期异常,出错的 url [{0}],出错的描述为 [{1}]",
request.getRequestURL().toString(), e.getMessage()), e);

return new Response.ResponseBuilder<>(ResponseCodeEnum.Internal.INTERNAL_ERROR).build();
}
}

Filter过滤器,对拦截器的异常进行拦截,封装成Response返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j
public class GlobalExceptionFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
try {
chain.doFilter(request, response);
} catch (ServletException ex) {
log.error("global filter exceptions", ex);
String resultJson = JsonUtil.toJson(Response.fail(ex));
writeToResponse(response, resultJson);
} catch (Exception e) {
log.error("global filter exceptions, unknown error:", e);
Response resp = Response.fail(new ServiceException(ResponseCodeEnum.Internal.INTERNAL_ERROR, e));
String resultJson = JsonUtil.toJson(resp);
writeToResponse(response, resultJson);
}
}

private void writeToResponse(ServletResponse response, String resultJson) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(resultJson);
}

@Override
public void destroy() {

}
}

响应基类BaseResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@AllArgsConstructor
@NoArgsConstructor
@Data
@ApiModel("响应基础返回值")
public class BaseResult {

@ApiModelProperty(name = "结果码", notes = "正确响应时该值为XXXX,错误响应时为错误代码")
@JsonProperty(value = "code", required = true)
protected int code;

@ApiModelProperty(name = "结果码的消息描述", notes = "人工可读的消息")
protected String msg;
}

统一响应格式Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel("统一Response返回值")
@Slf4j
public class Response<T> extends BaseResult {

/**
* 请求方传入的额外参数,响应中通过key=externalExt的形式返回,放到payload Map结构中返回
*/
public static final String EXTERNAL_EXT = "externalExt";

/**
* 业务数据
*/
@JsonProperty("data")
@JsonInclude
@ApiModelProperty(name = "响应体", notes = "正确响应时该值会被使用")
@Nullable
private T data;

/**
* 当验证错误时,各项具体的错误信息
*/
@JsonProperty(value = "errors", required = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModelProperty("错误信息")
@Nullable
private List<Error> errors;

/**
* 额外数据,默认将客户端传来的数据返回去
*/
@JsonProperty(value = "payload", required = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModelProperty("额外数据")
@Nullable
private Map<String, Object> payload;

private Response(ResponseBuilder<T> builder) {
super(builder.getCode(), builder.getMsg());
this.data = builder.getData();
this.errors = builder.getErrors();
this.payload = builder.getPayload();
}

// 提供一些常用的静态方法通过ResponseBuilder构建各种类型的响应,如果不合适则可以通过Builder定制
// 成功
public static Response success() {
return new ResponseBuilder(ResponseCodeEnum.SUCCESS).build();
}

public static <T> Response<T> success(T data) {
return new ResponseBuilder<T>(ResponseCodeEnum.SUCCESS)
.data(data)
.build();
}

// 中间状态
public static Response pending() {
return new ResponseBuilder<>(ResponseCodeEnum.PENDING).build();
}

public static <T> Response<T> pending(T data) {
return new ResponseBuilder<T>(ResponseCodeEnum.PENDING)
.data(data)
.build();
}

// 失败
public static <T> Response<T> fail() {
return new ResponseBuilder<T>(ResponseCodeEnum.FAILED).build();
}

public static <T> Response<T> fail(T data) {
return new ResponseBuilder<T>(ResponseCodeEnum.FAILED)
.data(data)
.build();
}

public static <T> Response<T> fail(ServiceException e) {
return new ResponseBuilder<T>(e.getResponseCode())
.payload(e.getPayload())
.build();
}

public static <T> Response<T> fail(ServiceException e, T data) {
return new ResponseBuilder<T>(e.getResponseCode())
.payload(e.getPayload())
.data(data)
.build();
}

/**
* 统一响应类Response建造者
* 依赖于Spring Context,支持响应结果码描述国际化,参数替换。
*
* @param <T>
*/
public static class ResponseBuilder<T> {
private final ResponseCodeInterface responseCode;
/**
* 国际化的结果码信息内容
*/
private final String msg;
private final int code;
private T data;
private List<Error> errors;
private Map<String, Object> payload;

public Response<T> build() {
return new Response<>(this);
}

public ResponseBuilder(ResponseCodeInterface responseCode, Object... args) {
this.responseCode = responseCode;
this.code = responseCode.code();
// 替换默认描述占位符后的默认错误描述
String defaultMsg = MessageFormatter.arrayFormat(responseCode.message(), args).getMessage();
String i18nMessage = null;
try {
// 获取国际化的信息描述,如果配置responseCode.i18nKey()对应的key-value则支持国际化。也可以临时对默认的错误描述进行覆盖。
i18nMessage = MessageUtils.message(responseCode.i18nKey(), args);
} catch (BeansException e) {
log.error("MessageUtils not ready, depends on messageSource", e);
} catch (Exception e) {
log.error("could not found i18nMessage entry for key: " + responseCode.i18nKey(), e);
}

this.msg = StringUtils.isNotBlank(i18nMessage) ? i18nMessage : defaultMsg;
}

/**
* 通过ServiceException业务异常创建Builder
* 注:在ServiceException中已经进行错误描述的国际化处理
*
* @param e
*/
public ResponseBuilder(ServiceException e) {
this.responseCode = e.getResponseCode();
this.code = e.getResponseCode().code();
this.msg = e.getMessage();
this.payload = e.getPayload();
}


public int getCode() {
return this.code;
}

public String getMsg() {
return this.msg;
}

public ResponseCodeInterface getResponseCode() {
return responseCode;
}

public ResponseBuilder<T> data(T data) {
this.data = data;
return this;
}

public T getData() {
return data;
}

public ResponseBuilder<T> errors(List<Error> errors) {
this.errors = errors;
return this;
}

public List<Error> getErrors() {
return errors;
}

public ResponseBuilder<T> addError(Error error) {
if (this.errors == null) {
this.errors = new ArrayList<>();
}
this.errors.add(error);
return this;
}

public ResponseBuilder<T> addPayloadAttr(String key, String value) {
if (this.payload == null) {
this.payload = new HashMap<>();
}
this.payload.put(key, value);
return this;
}

public Map<String, Object> getPayload() {
return payload;
}

public ResponseBuilder<T> payload(Map<String, Object> payload) {
this.payload = payload;
return this;
}

public ResponseBuilder<T> clientExt(Map<String, Object> clientExt) {
if (this.payload == null) {
this.payload = new HashMap<>();
}
this.payload.put(EXTERNAL_EXT, clientExt);
return this;
}
}

@Data
@AllArgsConstructor
@ApiModel("统一Response返回值中错误信息的模型")
public static class Error {
@ApiModelProperty(name = "错误项", notes = "错误的具体项")
private String name;
@ApiModelProperty(name = "错误项说明", notes = "错误的具体项说明")
private String message;
}
}

思考

响应码映射

通常一个后端服务还会去调用第三方,比如一个聚合支付平台后面(南向)有各种支付渠道,系统层面也会依赖分布式缓存(Redis),配置中心,消息队列等内部服务,因此针对这些调用报错,后端不会直接透传,而是会进行包装,第三方错误码映射后才会返回给调用方。这部分设计暂时没有纳入进来,如果要做,下面是一些考虑的点:

todo:
1.依赖方的响应码(南向)到调用方(北向)响应码的映射计划通过国际化的响应码配置文件中定义。key的组成为<被调用方ID>.<服务类型Type>.<南向响应码resultCode>=<北向响应码code>,需要提供code到枚举的解析方法,如果考虑合理存放响应码映射信息,可以通过一个resultCode.properties来存放。示例:Alipay.[Charge.]E0001=10601
2.通过数据库,适合支持运营配置的场景。
3.通过代码逻辑+枚举类映射的方式(变更的代价较大,一般不建议)

响应码映射的管理,如果需要支持运营配置,可以考虑将响应码映射的维护放在数据库,应该支持以下功能:

  • 定制化南向响应码对应的北向映射code,message
  • 支持添加北向响应码和响应码描述定义
  • 支持配置南向到北向的映射

HTTP状态码

Rest风格的API设计后,httpcode充当什么角色?主流的都是服务器内部异常时500,客户端错误400,请求到达服务端后代码流程正常(异常分支处理也属于代码正常流程)则统一返回200。几点思考:

  • 根据处理异常类型设置httpcode?
  • 结合nginx健康检查?到后端之后统一200?—- 划分为内部资源,通常只要流程处理正常,就返回2XX,重定向则3XX,客户端问题就4XX,服务端资源不足了那就5XX,外部资源可以返回2XX然后通过下面的业务结果告知客户端具体错误

总结

统一响应格式,主要方便前后端沟通,规范化后端研发人员的编码;全局异常处理是优雅的处理各种报错,从业务逻辑中分离开来,使得研发更关注业务;国际化是特殊处理,如果要提供全球化的服务,那么是值得做的。

代码:todo待整理上传github

参考

HTTP:

Hypertext Transfer Protocol (HTTP) Status Code Registry
Hypertext Transfer Protocol – HTTP/1.1
火狐官方HTTP定义
GitHub REST API 文档

API文档:

Twitter Ads API
Paypal API

声明:本站所有文章均为原创或翻译,遵循署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,如需转载请确保您对该协议有足够了解,并附上作者名 (Tsukasa) 及原文地址