Spring Boot 全局异常处理
ErrorCode.java
(此枚举类中包含了异常的唯一标识、HTTP 状态码以及错误信息)
这个类的主要作用就是统一管理系统中可能出现的异常,比较清晰明了。但是,可能出现的问题是当系统过于复杂,出现的异常过多之后,这个类会比较庞大。有一种解决办法:将多种相似的异常统一为一个,比如将用户找不到异常和订单信息未找到的异常都统一为“未找到该资源”这一种异常,然后前端再对相应的情况做详细处理。
import org.springframework.http.HttpStatus;public enum ErrorCode {/*** 注意写错误码的几点:* 1.是 public enum 不是 public class* 2.只需要写get方法和全构造* 3.错误参数构造之间用逗号隔开*/RESOURCE_NOT_FOUND(1001, HttpStatus.NOT_FOUND, "未找到该资源"),REQUEST_VALIDATION_FAILED(1002, HttpStatus.BAD_REQUEST, "请求数据格式验证失败");private final int code;private final HttpStatus status;private final String message;ErrorCode(int code, HttpStatus status, String message) {this.code = code;this.status = status;this.message = message;}public int getCode() {return code;}public HttpStatus getStatus() {return status;}public String getMessage() {return message;}@Overridepublic String toString() {return"ErrorCode{" +"code=" + code +", status=" + status +", message='" + message + '\'' +'}';}
}
ErrorResponse.java
(返回给客户端具体的异常对象)
这个类作为异常信息返回给客户端,里面包括了当出现异常时我们想要返回给客户端的所有信息。
import org.springframework.util.ObjectUtils;import java.time.Instant;
import java.util.HashMap;
import java.util.Map;public class ErrorResponse {// 唯一标示异常的 codeprivate int code;// HTTP状态码private int status;// 错误的具体信息private String message;// 错误路径private String path;// 发生错误的时间戳private Instant timestamp;private HashMap<String, Object> data = new HashMap<String, Object>();public ErrorResponse() {}public ErrorResponse(BaseException ex, String path) {this(ex.getError().getCode(), ex.getError().getStatus().value(), ex.getError().getMessage(), path, ex.getData());}public ErrorResponse(int code, int status, String message, String path, Map<String, Object> data) {this.code = code;this.status = status;this.message = message;this.path = path;this.timestamp = Instant.now();if (!ObjectUtils.isEmpty(data)) {this.data.putAll(data);}}// 省略 getter/setter 方法@Overridepublic String toString() {return"ErrorReponse{" +"code=" + code +", status=" + status +", message='" + message + '\'' +", path='" + path + '\'' +", timestamp=" + timestamp +", data=" + data +'}';}
}
BaseException.java
(继承自 RuntimeException
的抽象类,可以看做系统中其他异常类的父类)
系统中的异常类都要继承自这个类
public abstract class BaseException extends RuntimeException {private final ErrorCode error;private final HashMap<String, Object> data = new HashMap<>();public BaseException(ErrorCode error, Map<String, Object> data) {super(error.getMessage());this.error = error;if (!ObjectUtils.isEmpty(data)) {this.data.putAll(data);}}protected BaseException(ErrorCode error, Map<String, Object> data, Throwable cause) {super(error.getMessage(), cause);this.error = error;if (!ObjectUtils.isEmpty(data)) {this.data.putAll(data);}}public ErrorCode getError() {return error;}public Map<String, Object> getData() {return data;}}
ResourceNotFoundException.java
(自定义异常)
可以看出通过继承 BaseException
类自定义异常会变的非常简单!
import java.util.Map;public class ResourceNotFoundException extends BaseException {public ResourceNotFoundException(Map<String, Object> data) {super(ErrorCode.RESOURCE_NOT_FOUND, data);}
}
GlobalExceptionHandler.java
(全局异常捕获)
定义了两个异常捕获方法。
这里再说明一下,实际上这个类只需要 handleAppException()
这一个方法就够了,因为它是本系统所有异常的父类。只要是抛出了继承 BaseException
类的异常后都会在这里被处理。
import com.twuc.webApp.web.ExceptionController;
import org.springframework.http.HttpHeaders;
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.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {// 也可以将 BaseException 换为 RuntimeException// 因为 RuntimeException 是 BaseException 的父类@ExceptionHandler(BaseException.class)public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {ErrorReponse representation = new ErrorReponse(ex, request.getRequestURI());return new ResponseEntity<>(representation, new HttpHeaders(), ex.getError().getStatus());}@ExceptionHandler(value = ResourceNotFoundException.class)public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {ErrorReponse errorReponse = new ErrorReponse(ex, request.getRequestURI());return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorReponse);}
}
当抛出了ResourceNotFoundException
异常,会被handleResourceNotFoundException()
方法捕获。因为 @ExceptionHandler
捕获异常的过程中,会优先找到最匹配的。
补充内容:
@ControllerAdvice可以指定捕获异常的控制器范围,比如这里的assignableTypes=ExceptionController.class,表示只处理ExceptionController抛出的异常。不指定的话默认对所有controller有效。
可以在全局异常处理方法中获取更多上下文信息,如请求参数,用户信息等,方便异常处理和日志记录。
可以定义不同的全局异常处理类拦截不同范围的异常,比如定义一个全局未捕获异常处理器,一个控制器异常处理器等。
/*** 定义一个全局未捕获异常处理器类* 该异常处理器的优先级低于其他具体的异常处理方法* 可以配合其他具体异常处理方法,处理未覆盖到的异常情况* 可以保证系统兜底捕获所有异常,避免未处理的异常直接抛出到用户*/ @ControllerAdvice public class GlobalExceptionHandler {// 通过Exception.class参数捕获一切Exception类及其子类的异常@ExceptionHandler(value = Exception.class) public ResponseEntity<String> handleException(Exception e){// 处理未捕获的异常return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); }}
/*** 在控制器类中定义异常处理方法,加上@ExceptionHandler注解* 与全局异常处理器配合使用,控制器内的异常处理器优先级更高* 这样可以在控制器层面处理控制器范围内的特定异常* 所以Controller中的异常处理器可以更加具体地处理控制器业务场景下的异常情况*/ @RestController public class UserController {// 该方法可以捕获在本控制器类中的UserNotFoundException异常@ExceptionHandler(UserNotFoundException.class)public ResponseEntity<String> handleUserNotFound(UserNotFoundException e) {// ...return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");}}
如上例可以使用@ExceptionHandler在控制器内处理控制器范围内的异常,与全局异常处理器配合使用。
正确设置HTTP状态码很重要,比如404找不到资源等,有助于客户端判断异常情况。
可以在异常处理方法中获取bindingResult,分析参数校验的错误信息。
返回给客户端的错误信息体中可以包含代码、信息等字段,以便客户端准确判断和处理。