Spring REST错误处理示例
Spring REST 错误处理示例
本文旨在展示如何在 Spring Boot REST 应用程序中实现错误处理。我们将探讨默认的错误处理机制、自定义异常处理、验证错误处理以及全局错误属性的定制。
使用的技术栈:
- Spring Boot 2.1.2.RELEASE
- Spring 5.1.4.RELEASE
- Maven 3
- Java 8
1. 默认错误处理 (/error)
1.1 BasicErrorController
默认情况下,Spring Boot 提供了一个 BasicErrorController,用于映射 /error 路径以处理所有错误。该控制器调用 getErrorAttributes 方法,生成一个包含错误详细信息、HTTP 状态码和异常消息的 JSON 响应。
默认响应示例如下:
{
"timestamp": "2019-02-27T04:03:52.398+0000",
"status": 500,
"error": "Internal Server Error",
"message": "...",
"path": "/path"
}以下是 BasicErrorController 的核心逻辑片段:
package org.springframework.boot.autoconfigure.web.servlet.error;
//...
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//...
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
}调试技巧:在 IDE 中在此方法处放置断点,您可以直观地了解 Spring Boot 如何生成默认的 JSON 错误响应。
2. 自定义异常处理 (Custom Exception)
在 Spring Boot 中,我们可以使用 @ControllerAdvice 结合 @ExceptionHandler 来处理自定义异常。
2.1 定义自定义异常
首先,定义一个运行时异常,用于表示图书未找到的情况。
BookNotFoundException.java
package com.jsdiff.error;
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book id not found : " + id);
}
}2.2 控制器抛出异常
当未找到图书时,控制器将抛出上述 BookNotFoundException。
BookController.java
package com.jsdiff;
//...
@RestController
public class BookController {
@Autowired
private BookRepository repository;
// Find
@GetMapping("/books/{id}")
Book findOne(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new BookNotFoundException(id));
}
//...
}默认情况下,Spring Boot 会将未捕获的运行时异常视为服务器内部错误,生成 HTTP 500 响应:
curl localhost:8080/books/5{
"timestamp": "2019-02-27T04:03:52.398+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Book id not found : 5",
"path": "/books/5"
}2.3 重写状态码 (返回 404)
如果未找到图书,通常应返回 404 错误而不是 500 错误。我们可以通过 @ExceptionHandler 重写状态码。
CustomGlobalExceptionHandler.java (步骤 1)
package com.jsdiff.error;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@ControllerAdvice
public class CustomGlobalExceptionHandler {
// 让 Spring BasicErrorController 处理异常,我们仅重写状态码
@ExceptionHandler(BookNotFoundException.class)
public void springHandleNotFound(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.NOT_FOUND.value());
}
//...
}此时再次请求,将返回 404 状态:
curl localhost:8080/books/5{
"timestamp": "2019-02-27T04:21:17.740+0000",
"status": 404,
"error": "Not Found",
"message": "Book id not found : 5",
"path": "/books/5"
}2.4 自定义 JSON 错误响应结构
除了修改状态码,我们还可以完全自定义返回的 JSON 错误响应结构。
首先定义响应对象:
CustomErrorResponse.java
package com.jsdiff.error;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
public class CustomErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
private int status;
private String error;
//... getters setters
}然后修改全局异常处理器,返回自定义对象:
CustomGlobalExceptionHandler.java (步骤 2)
package com.jsdiff.error;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<CustomErrorResponse> customHandleNotFound(Exception ex, WebRequest request) {
CustomErrorResponse errors = new CustomErrorResponse();
errors.setTimestamp(LocalDateTime.now());
errors.setError(ex.getMessage());
errors.setStatus(HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errors, HttpStatus.NOT_FOUND);
}
//...
}响应结果如下:
curl localhost:8080/books/5{
"timestamp": "2019-02-27 12:40:45",
"status": 404,
"error": "Book id not found : 5"
}3. JSR 303 验证错误处理
对于 Spring @Valid 验证错误,框架通常会抛出 MethodArgumentNotValidException。我们可以通过继承 ResponseEntityExceptionHandler 并重写相应方法来处理。
CustomGlobalExceptionHandler.java
package com.jsdiff.error;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
// 用于验证路径变量和请求参数 (@Validate)
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.BAD_REQUEST.value());
}
// 处理 @Valid 注解引发的错误
@Override
protected ResponseEntity<Object>
handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", new Date());
body.put("status", status.value());
// 获取所有字段错误信息
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}4. ResponseEntityExceptionHandler 调试
如果您不确定 Spring Boot 会抛出何种异常,可以在 ResponseEntityExceptionHandler 的相关方法中放置断点进行调试。该类涵盖了多种常见的 Web 异常。
ResponseEntityExceptionHandler.java
package org.springframework.web.servlet.mvc.method.annotation;
//...
public abstract class ResponseEntityExceptionHandler {
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
HttpHeaders headers = new HttpHeaders();
if (ex instanceof HttpRequestMethodNotSupportedException) {
HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
}
//...
}
//...
}5. 全局错误属性定制 (DefaultErrorAttributes)
若要覆盖所有异常的默认 JSON 错误响应结构(例如修改时间格式或添加额外字段),可以创建一个 Bean 并扩展 DefaultErrorAttributes。
CustomErrorAttributes.java
package com.jsdiff.error;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 先让 Spring 处理错误,随后我们进行修改
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
// 格式化并更新时间戳
Object timestamp = errorAttributes.get("timestamp");
if (timestamp == null) {
errorAttributes.put("timestamp", dateFormat.format(new Date()));
} else {
errorAttributes.put("timestamp", dateFormat.format((Date) timestamp));
}
// 插入新字段
errorAttributes.put("version", "1.2");
return errorAttributes;
}
}配置完成后,日期时间将被格式化,并且 JSON 错误响应中会新增 version 字段。
curl localhost:8080/books/5{
"timestamp": "2019/02/27 13:34:24",
"status": 404,
"error": "Not Found",
"message": "Book id not found : 5",
"path": "/books/5",
"version": "1.2"
}curl localhost:8080/abc{
"timestamp": "2019/02/27 13:35:10",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/abc",
"version": "1.2"
}参考文献
- Spring Boot 错误处理参考
- DefaultErrorAttributes 文档
- ResponseEntityExceptionHandler
- Spring REST 验证示例
- Spring Boot 安全功能
- 带有引导的 Hello Spring Security
- 维基百科– REST
说明: 本文示例基于 Spring Boot 2.1.2 版本编写。Spring Boot 后续版本(如 2.3+ 或 3.x)在错误处理机制、默认属性结构及自动配置类上可能有所调整,请根据实际使用的版本参考官方文档进行适配。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/spring-rest-cuo-wu-chu-li-shi-li.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。