DoneSpeak

SpringMVC | Controller 返回值及异常的统一处理

字数: 1.6k时长: 8 min
2019/03/24 Share

旧的设计方案

开发api的时候,需要先定义好接口的数据响应结果.如下是一个很简单直接的Controller实现方法及响应结果定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/users")
public class UserController {

@Inject
private UserService userService;

@GetRequest("/{userId:\\d+}")
public ResponseBean signin(@PathVariable long userId) {
try {
User user = userService.getUserBaseInfo(userId);
return ResponseBean.success(user);
} catch (ServiceException e) {
return new ReponseBean(e.getCode(), e.getMsg());
} catch (Exception e) {
return ResponseBean.systemError();
}
}
}
1
2
3
4
5
{
code: "",
data: {}, // 可以是对象或者数组
msg: ""
}

从上面的代码,我们可以看到对于每个 Controller 方法,都会有很多重复的代码出现,我们应该设法去避免重复的代码。将重复的代码移除之后,可以得到如下的代码,简单易懂。

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/users")
public class UserController {

@Inject
private UserService userService;

@GetRequest("/{userId:\\d+}")
public User signin(@PathVariable long userId) {
return userService.getUserBaseInfo(userId);
}
}

在以上的实现中,还做了一个必要的要求,就是 ServiceException 需要定义为 RuntimeException的子类,而不是 Exception的子类。由于 ServiceException 表示服务异常,一般发生这种异常是应该直接提示前端,而无需进行其他特殊处理的。在定义为 RuntimeException 的子类之后,会减少大量的异常抛出声明,而且不再需要在事务@Transactional 中进行特殊声明。

统一 Controller 返回值格式

在开发的过程中,我发现上面的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ControllerAdvice
public class ControllerResponseHandler implements ResponseBodyAdvice<Object> {

private Logger logger = LogManager.getLogger(getClass());

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 支持所有的返回值类型
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if(body instanceof ResponseBean) {
return body;
} else {
// 所有没有返回 ResponseBean 结构的结果均认为是成功的
return ResponseBean.success(body);
}
}
}

统一异常处理

如下的代码中,ServiceException ServiceMessageException ValidatorErrorType FieldValidatorError 均为自定义类。

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
@ControllerAdvice
public class ControllerExceptionHandler {

private Logger logger = LogManager.getLogger(getClass());

private static final String logExceptionFormat = "[EXIGENCE] Some thing wrong with the system: %s";

/**
* 自定义异常
*/
@ExceptionHandler(ServiceMessageException.class)
public ResponseBean handleServiceMessageException(HttpServletRequest request, ServiceMessageException ex) {
logger.debug(ex);
return new ResponseBean(ex.getMsgCode(), ex.getMessage());
}

/**
* 自定义异常
*/
@ExceptionHandler(ServiceException.class)
public ResponseBean handleServiceException(HttpServletRequest request, ServiceException ex) {
logger.debug(ex);
String message = codeToMessage(ex.getMsgCode());
return new ResponseBean(ex.getMsgCode(), message);
}

/**
* MethodArgumentNotValidException: 实体类属性校验不通过
* 如: listUsersValid(@RequestBody @Valid UserFilterOption option)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseBean handleMethodArgumentNotValid(HttpServletRequest request, MethodArgumentNotValidException ex) {
logger.debug(ex);
return validatorErrors(ex.getBindingResult());
}

private ResponseBean validatorErrors(BindingResult result) {
List<FieldValidatorError> errors = new ArrayList<FieldValidatorError>();
for (FieldError error : result.getFieldErrors()) {
errors.add(toFieldValidatorError(error));
}
return ResponseBean.validatorError(errors);
}

/**
* ConstraintViolationException: 直接对方法参数进行校验,校验不通过。
* 如: pageUsers(@RequestParam @Min(1)int pageIndex, @RequestParam @Max(100)int pageSize)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseBean handleConstraintViolationException(HttpServletRequest request,
ConstraintViolationException ex) {
logger.debug(ex);
//
List<FieldValidatorError> errors = new ArrayList<FieldValidatorError>();

for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(toFieldValidatorError(violation));
}
return ResponseBean.validatorError(errors);
}

private FieldValidatorError toFieldValidatorError(ConstraintViolation<?> violation) {
Path.Node lastNode = null;
for (Path.Node node : violation.getPropertyPath()) {
lastNode = node;
}

FieldValidatorError fieldNotValidError = new FieldValidatorError();
// fieldNotValidError.setType(ValidatorTypeMapping.toType(violation.getConstraintDescriptor().getAnnotation().annotationType()));
fieldNotValidError.setType(ValidatorErrorType.INVALID.value());
fieldNotValidError.setField(lastNode.getName());
fieldNotValidError.setMessage(violation.getMessage());
return fieldNotValidError;
}

private FieldValidatorError toFieldValidatorError(FieldError error) {
FieldValidatorError fieldNotValidError = new FieldValidatorError();
fieldNotValidError.setType(ValidatorErrorType.INVALID.value());
fieldNotValidError.setField(error.getField());
fieldNotValidError.setMessage(error.getDefaultMessage());
return fieldNotValidError;
}

/**
* BindException: 数据绑定异常,效果与MethodArgumentNotValidException类似,为MethodArgumentNotValidException的父类
*/
@ExceptionHandler(BindException.class)
public ResponseBean handleBindException(HttpServletRequest request, BindException ex) {
logger.debug(ex);
return validatorErrors(ex.getBindingResult());
}

/**
* 返回值类型转化错误
*/
@ExceptionHandler(HttpMessageConversionException.class)
public ResponseBean exceptionHandle(HttpServletRequest request,
HttpMessageConversionException ex) {
return internalServiceError(ex);
}

/**
* 对应 Http 请求头的 accept
* 客户器端希望接受的类型和服务器端返回类型不一致。
* 这里虽然设置了拦截,但是并没有起到作用。需要通过http请求的流程来进一步确定原因。
*/
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseBean handleHttpMediaTypeNotAcceptableException(HttpServletRequest request,
HttpMediaTypeNotAcceptableException ex) {
logger.debug(ex);
StringBuilder messageBuilder = new StringBuilder().append("The media type is not acceptable.")
.append(" Acceptable media types are ");
ex.getSupportedMediaTypes().forEach(t -> messageBuilder.append(t + ", "));
String message = messageBuilder.substring(0, messageBuilder.length() - 2);

return new ResponseBean(HttpStatus.NOT_ACCEPTABLE.value(), message);
}

/**
* 对应请求头的 content-type
* 客户端发送的数据类型和服务器端希望接收到的数据不一致
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseBean handleHttpMediaTypeNotSupportedException(HttpServletRequest request,
HttpMediaTypeNotSupportedException ex) {
logger.debug(ex);
StringBuilder messageBuilder = new StringBuilder().append(ex.getContentType())
.append(" media type is not supported.").append(" Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> messageBuilder.append(t + ", "));
String message = messageBuilder.substring(0, messageBuilder.length() - 2);
System.out.println(message);
return new ResponseBean(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), message);
}

/**
* 前端发送过来的数据无法被正常处理
* 比如后天希望收到的是一个json的数据,但是前端发送过来的却是xml格式的数据或者是一个错误的json格式数据
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseBean handlerHttpMessageNotReadableException(HttpServletRequest request,
HttpMessageNotReadableException ex) {
logger.debug(ex);
String message = "Problems parsing JSON";
return new ResponseBean(HttpStatus.BAD_REQUEST.value(), message);
}

/**
* 将返回的结果转化到响应的数据时候导致的问题。
* 当使用json作为结果格式时,可能导致的原因为序列化错误。
* 目前知道,如果返回一个没有属性的对象作为结果时,会导致该异常。
*/
@ExceptionHandler(HttpMessageNotWritableException.class)
public ResponseBean handlerHttpMessageNotWritableException(HttpServletRequest request,
HttpMessageNotWritableException ex) {
return internalServiceError(ex);
}

/**
* 请求方法不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) {
logger.debug(ex);
StringBuilder messageBuilder = new StringBuilder().append(ex.getMethod())
.append(" method is not supported for this request.").append(" Supported methods are ");

ex.getSupportedHttpMethods().forEach(m -> messageBuilder.append(m + ","));
String message = messageBuilder.substring(0, messageBuilder.length() - 2);
return new ResponseBean(HttpStatus.METHOD_NOT_ALLOWED.value(), message);
}

/**
* 参数类型不匹配
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseBean methodArgumentTypeMismatchExceptionHandler(HttpServletRequest request,
MethodArgumentTypeMismatchException ex) {
logger.debug(ex);
String message = "The parameter '" + ex.getName() + "' should of type '"
+ ex.getRequiredType().getSimpleName().toLowerCase() + "'";

FieldValidatorError fieldNotValidError = new FieldValidatorError();
fieldNotValidError.setType(ValidatorErrorType.TYPE_MISMATCH.value());
fieldNotValidError.setField(ex.getName());
fieldNotValidError.setMessage(message);

return ResponseBean.validatorError(Arrays.asList(fieldNotValidError));
}

/**
* 缺少必填字段
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseBean exceptionHandle(HttpServletRequest request,
MissingServletRequestParameterException ex) {
logger.debug(ex);
String message = "Required parameter '" + ex.getParameterName() + "' is not present";

FieldValidatorError fieldNotValidError = new FieldValidatorError();
fieldNotValidError.setType(ValidatorErrorType.MISSING_FIELD.value());
fieldNotValidError.setField(ex.getParameterName());
fieldNotValidError.setMessage(message);

return ResponseBean.validatorError(Arrays.asList(fieldNotValidError));
}

/**
* 文件上传时,缺少 file 字段
*/
@ExceptionHandler(MissingServletRequestPartException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, MissingServletRequestPartException ex) {
logger.debug(ex);
return new ResponseBean(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
}

/**
* 请求路径不存在
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, NoHandlerFoundException ex) {
logger.debug(ex);
String message = "No resource found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
return new ResponseBean(HttpStatus.NOT_FOUND.value(), message);
}

/**
* 缺少路径参数
* Controller方法中定义了 @PathVariable(required=true) 的参数,但是却没有在url中提供
*/
@ExceptionHandler(MissingPathVariableException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, MissingPathVariableException ex) {
return internalServiceError(ex);
}

/**
* 其他所有的异常
*/
@ExceptionHandler()
public ResponseBean handleAll(HttpServletRequest request, Exception ex) {
return internalServiceError(ex);
}

private String codeToMessage(int code) {
//TODO 这个需要进行自定,每个 code 会匹配到一个相应的 msg
return "The code is " + code;
}

private ResponseBean internalServiceError(Exception ex) {
logException(ex);
// do something else
return ResponseBean.systemError();
}

private <T extends Throwable> void logException(T e) {
logger.error(String.format(logExceptionFormat, e.getMessage()), e);
}
}

通过上面的配置,可以有效地将异常进行统一的处理,同时对返回的结果进行统一的封装。

CATALOG
  1. 1. 旧的设计方案
  2. 2. 统一 Controller 返回值格式
  3. 3. 统一异常处理