1. 概述
本教程将讨论如何为 Spring REST API 实现一个全局异常处理器。
我们的目标是根据每个异常的语义,构建出对客户端有意义的错误信息,让客户端能够轻松诊断问题。
2. 自定义错误消息结构
我们先来实现一个简单的错误信息传输结构 —— ApiError:
public class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
public ApiError(HttpStatus status, String message, List<String> errors) {
super();
this.status = status;
this.message = message;
this.errors = errors;
}
public ApiError(HttpStatus status, String message, String error) {
super();
this.status = status;
this.message = message;
errors = Arrays.asList(error);
}
}
字段含义如下:
- status – HTTP 状态码
- message – 与异常相关的错误信息
- errors – 构造的错误消息列表
在 Spring 中处理异常时,我们将使用 @ControllerAdvice 注解:
@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
...
}
3. 处理 Bad Request 异常
3.1. 异常处理逻辑
我们先来看最常见的客户端错误处理场景 —— 客户端发送了无效请求:
✅ BindException 和 MethodArgumentNotValidException
当方法参数使用 @Valid
注解校验失败时会抛出 MethodArgumentNotValidException
:
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
List<String> errors = new ArrayList<String>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.add(error.getField() + ": " + error.getDefaultMessage());
}
for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return handleExceptionInternal(
ex, apiError, headers, apiError.getStatus(), request);
}
⚠️ 注意:我们是在重写
ResponseEntityExceptionHandler
中的方法,实现自己的处理逻辑。
✅ MissingServletRequestPartException 和 MissingServletRequestParameterException
分别用于处理 multipart 请求缺失部分和请求参数缺失的情况:
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {
String error = ex.getParameterName() + " parameter is missing";
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
✅ ConstraintViolationException
用于处理 Bean Validation 中的约束违反:
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
List<String> errors = new ArrayList<String>();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
✅ MethodArgumentTypeMismatchException
用于处理参数类型不匹配的情况:
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity<Object> handleMethodArgumentTypeMismatch(
MethodArgumentTypeMismatchException ex, WebRequest request) {
String error =
ex.getName() + " should be of type " + ex.getRequiredType().getName();
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
3.2. 客户端调用示例
我们来看一个测试,模拟 MethodArgumentTypeMismatchException:
@Test
public void whenMethodArgumentMismatch_thenBadRequest() {
Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("should be of type"));
}
发送的请求如下:
Request method: GET
Request path: http://localhost:8080/spring-security-rest/api/foos/ccc
返回的 JSON 错误信息如下:
{
"status": "BAD_REQUEST",
"message":
"Failed to convert value of type [java.lang.String]
to required type [java.lang.Long]; nested exception
is java.lang.NumberFormatException: For input string: \"ccc\"",
"errors": [
"id should be of type java.lang.Long"
]
}
4. 处理 NoHandlerFoundException
我们可以配置 Servlet,使其在找不到处理方法时抛出异常而不是返回 404:
<servlet>
<servlet-name>api</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
然后就可以像其他异常一样处理:
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
return new ResponseEntity<Object>(apiError, new HttpHeaders(), apiError.getStatus());
}
测试代码:
@Test
public void whenNoHandlerForHttpRequest_thenNotFound() {
Response response = givenAuth().delete(URL_PREFIX + "/api/xx");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.NOT_FOUND, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("No handler found"));
}
请求如下:
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/xx
错误响应如下:
{
"status":"NOT_FOUND",
"message":"No handler found for DELETE /spring-security-rest/api/xx",
"errors":[
"No handler found for DELETE /spring-security-rest/api/xx"
]
}
5. 处理 HttpRequestMethodNotSupportedException
当客户端使用了不支持的 HTTP 方法时会抛出该异常:
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getMethod());
builder.append(
" method is not supported for this request. Supported methods are ");
ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED,
ex.getLocalizedMessage(), builder.toString());
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
测试代码:
@Test
public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() {
Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("Supported methods are"));
}
请求如下:
Request method: DELETE
Request path: http://localhost:8080/spring-security-rest/api/foos/1
错误响应如下:
{
"status":"METHOD_NOT_ALLOWED",
"message":"Request method 'DELETE' not supported",
"errors":[
"DELETE method is not supported for this request. Supported methods are GET "
]
}
6. 处理 HttpMediaTypeNotSupportedException
当客户端发送了不支持的媒体类型时抛出该异常:
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
HttpMediaTypeNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getContentType());
builder.append(" media type is not supported. Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE,
ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
测试代码:
@Test
public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}
请求如下:
Request method: POST
Request path: http://localhost:8080/spring-security-
Headers: Content-Type=text/plain; charset=ISO-8859-1
错误响应如下:
{
"status":"UNSUPPORTED_MEDIA_TYPE",
"message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
"errors":["text/plain;charset=ISO-8859-1 media type is not supported.
Supported media types are text/xml
application/x-www-form-urlencoded
application/*+xml
application/json;charset=UTF-8
application/*+json;charset=UTF-8 */"
]
}
7. 默认异常处理器
最后,我们实现一个兜底的异常处理器,用于处理未被其他方法覆盖的异常:
@ExceptionHandler({ Exception.class })
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
ApiError apiError = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
return new ResponseEntity<Object>(
apiError, new HttpHeaders(), apiError.getStatus());
}
8. 总结
构建一个成熟、完善的 Spring REST API 异常处理器并非一蹴而就,需要在实践中不断迭代。希望本教程能为你提供一个良好的起点,帮助客户端快速定位和解决问题。
完整的代码示例可以在这里找到:GitHub 项目地址。这是一个基于 Eclipse 的项目,可直接导入运行。