Spring Boot是如何处理异常的
恰当的异常处理是保证程序健壮不可或缺的部分。Spring提供了多种方式来处理异常。
Java的异常逻辑
众所周知,Java中的异常是派生于Throwable
类的一个实例,分为Checked Exception
和Unchecked Exception
,如下图所示:
所有从RuntimeException
中继承的异常,属于Unchecked Exception
,是程序的逻辑错误,例如NullPointException
,ArrayIndexOutOfBoundsException
异常等。因此,只要出现RuntimeException
,那一定是你的问题。
Error
类型的异常,通常是Java运行时内部出现的错误和资源耗尽的错误,我们编写的代码不应该抛出这种类型的错误。
Checked Exception
,表示程序本身没有问题,但是由于IO错误导致的异常,这种异常是必须要处理的。
Spring异常处理机制
Spring中要处理的通常是RuntimeException
的子类,我们通常不去主动捕获这些异常,而在程序的某一个位置统一处理。Spring Boot为我们提供了一系列的注解,用来处理这些异常:
- @ResponseStatus
- @ExceptionHandler
- @ControllerAdvice
如果我们不启用这些注解,Spring处理异常通常有默认的行为逻辑,抛出的异常通常会报500
错误。
@ResponseStatus
允许我们去修改HTTP的返回码,我们可以把这个注解用在异常类上:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}
抛出这个异常会改变HTTP的返回码,如下所示:
{
"timestamp": "2020-11-29T09:42:12.287+00:00",
"status": 404,
"error": "Not Found",
"message": "Item with id 1 not found",
"path": "/product/1"
}
我们还可以通过继承ResponseStatusException
的方式来修改HTTP的返回码:
public class NoSuchElementFoundException extends ResponseStatusException {
public NoSuchElementFoundException(String message){
super(HttpStatus.NOT_FOUND, message);
}
@Override
public HttpHeaders getResponseHeaders() {
// return response headers
}
}
此时,还可以通过getResponseHeaders
方法来修改返回的头部。
@ExceptionHandler
注解可以修改返回的数据类型,可以用在Controller里面或者@ControllerAdvice
注解修饰的类里面。
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
//constructor omitted for brevity...
@GetMapping("/{id}")
public Response getProduct(@PathVariable String id) {
return productService.getProduct(id);
}
@ExceptionHandler(NoSuchElementFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<String> handleNoSuchElementFoundException(
NoSuchElementFoundException exception
) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(exception.getMessage());
}
}
@ControllerAdvice
如下所示;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handlerError(MissingServletRequestParameterException e) {
String message = String.format("Missing Request Parameter: %s", e.getParameterName());
return BaseResponse
.builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(MethodArgumentTypeMismatchException e) {
log.warn("Method Argument Type Mismatch", e);
String message = String.format("Method Argument Type Mismatch: %s", e.getName());
return BaseResponse
.builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(MethodArgumentNotValidException e) {
log.warn("Method Argument Not Valid", e);
BindingResult result = e.getBindingResult();
FieldError error = result.getFieldError();
String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
return BaseResponse
.builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(BindException e) {
log.warn("Bind Exception", e);
FieldError error = e.getFieldError();
assert error != null;
String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
return BaseResponse
.builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(ConstraintViolationException e) {
log.warn("Constraint Violation", e);
ConstraintViolation<?> violation = e.getConstraintViolations().iterator().next();
String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
String message = String.format("%s:%s", path, violation.getMessage());
return BaseResponse
.builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public BaseResponse handleError(NoHandlerFoundException e) {
log.error("404 Not Found", e);
return BaseResponse
.builder()
.message(e.getMessage())
.code(HttpStatus.NOT_FOUND.value())
.build();
}
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(HttpMessageNotReadableException e) {
log.error("Message Not Readable", e);
return BaseResponse
.builder()
.message(e.getMessage())
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public BaseResponse handleError(HttpRequestMethodNotSupportedException e) {
log.error("Request Method Not Supported", e);
return BaseResponse
.builder()
.message(e.getMessage())
.code(HttpStatus.METHOD_NOT_ALLOWED.value())
.build();
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public BaseResponse handleError(HttpMediaTypeNotSupportedException e) {
log.error("Media Type Not Supported", e);
return BaseResponse
.builder()
.message(e.getMessage())
.code(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value())
.build();
}
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResponse handleError(ValidationException e) {
log.error("Parameter error", e);
return BaseResponse.builder()
.message(e.getMessage())
.code(HttpStatus.BAD_REQUEST.value())
.build();
}
}
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
public static final String TRACE = "trace";
@Value("${reflectoring.trace:false}")
private boolean printStackTrace;
@Override
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request
) {
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
@ExceptionHandler(ItemNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<Object> handleItemNotFoundException(
ItemNotFoundException itemNotFoundException,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<Object> handleAllUncaughtException(
RuntimeException exception,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
//....
@Override
public ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return buildErrorResponse(ex,status,request);
}
}
Complete Guide to Exception Handling in Spring Boot