# 全局异常处理
本文介绍使用@ControllerAdvice对 Controller 抛出的异常进行统一拦截和处理。
# 定义返回格式
@Getter
@Setter
@AllArgsConstructor
public class UnifyResponse {
private int code;
private String message;
private String request;
}
首先定义一个统一的返回格式,所有的异常最终都按照统一格式返回给前端。
# 定义状态码
不同的异常对应不同的返回状态码
# exception-code.properties
demo.codes[0] = ok
demo.codes[9999] = 服务器未知异常
demo.codes[10000] = 通用错误
demo.codes[10001] = 通用参数错误
demo.codes[10002] = 资源未找到
首先将状态码集中在配置文件中进行管理
properties 的编码格式需要配置,否则可能出现中文乱码
IDEA 中 Preferences -> File Encodings -> Default encoding for properties files 设置为 UTF-8,同时勾上 Transparent native-to-ascii conversion
@Getter
@Setter
@ConfigurationProperties("demo")
@PropertySource("classpath:config/exception-code.properties")
@Component
public class ExceptionCodeConfiguration {
private Map<Integer, String> codes = new HashMap<>();
public String getMessage(int code) {
return codes.get(code);
}
}
其次使用类ExceptionCodeConfiguration实现对配置文件的访问,@PropertySource可以用来指定配置文件路径,@ConfigurationProperties用于指定配置前缀。
在配置类中,我们首先定义一个 Map 变量codes,这里的codes对应于配置文件中的 codes,SpringBoot 会在启动时自动将配置文件读入配置类中,最后需要使用@Component将配置类加入容器。
public final class ExceptionCode {
public static final int UNKNOWN = 9999;
public static final int COMMON = 10000;
public static final int PARAMS_ERROR = 10001;
public static final int RESOURCE_NOT_FOUND = 10002;
}
最后定义一个”常量类“便于状态码的调用,避免直接填入数字。
##通用异常
@ControllerAdvice根据异常类型的匹配程度选择相应的异常处理类,如果其他的 handler 都没有匹配到则使用通用的异常处理类。
@RestControllerAdvice
public class GlobalExceptionAdvice {
@Autowired
private ExceptionCodeConfiguration codeConfiguration;
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public UnifyResponse handleException(HttpServletRequest req, Exception e) {
return new UnifyResponse(ExceptionCode.UNKNOWN, codeConfiguration.getMessage(ExceptionCode.UNKNOWN), this.getRequest(req));
}
// 获取请求方法和路径
private String getRequest(HttpServletRequest req) {
String url = req.getRequestURI();
String method = req.getMethod();
return method + " " + url;
}
}
首先要在全局异常处理类上标注@ControllerAdvice注解,这里使用@RestControllerAdvice避免在方法上添加@ResponseBody。
然后定义一个方法handleException,同时在方法上添加@ExceptionHandler(value=Class<? extends Throwable>)注解,注解接收一个异常类型,表示该方法要处理什么异常,异常类型越具体,能够处理的范围越小。方法handleException接收HttpServletRequest和Exception两个参数类型,用于接收数据进行处理。
最终,对数据进行处理并打包成UnifyResponse进行返回。其中codeConfiguration是我们前面定义的异常码配置类,对应于我们自定义的异常码配置文件,通过这种方式我们可以获得 code 对应的 message。
# 自定义异常
通用异常处理一般是处理意料之外的异常,对于开发者有意抛出的异常,我们可以单独定义相应的异常类型便于使用和处理。
# 定义异常
@Getter
public class HttpException extends RuntimeException {
protected Integer code;
protected Integer httpStatusCode = 500;
}
public class NotFoundHttpException extends HttpException {
public NotFoundHttpException(int code) {
this.code = code;
this.httpStatusCode = 404;
}
}
这里我们自定义了HttpException用于开发者主动抛出,HttpException继承自RuntimeException,由于我们抛出异常后会被统一拦截处理,不希望编译期间进行检查,所以使用RuntimeException。
我们可以根据不同的 HTTP 状态码定义不同的异常类型,这里我们定义了NotFoundHttpException,他的状态码为 404,实际调用时我们只需要抛出相应异常并填入自定义状态码即可。
# 抛出自定义异常
@RestController
@Validated
public class UserController {
@GetMapping("/user/{id}")
public String getUser(@PathVariable @Max(20) Integer id, @RequestParam @Length(min = 2, max = 10) String name) {
throw new NotFoundHttpException(ExceptionCode.RESOURCE_NOT_FOUND);
}
假设没有找到用户,我们直接抛出NotFoundHttpException并填入自定义 code 即可完成错误处理。
# 全局异常处理
@RestControllerAdvice
public class GlobalExceptionAdvice {
@Autowired
private ExceptionCodeConfiguration codeConfiguration;
@ExceptionHandler(HttpException.class)
public ResponseEntity<UnifyResponse> handleHttpException(HttpServletRequest req, HttpException e) {
int code = e.getCode();
UnifyResponse response = new UnifyResponse(code, codeConfiguration.getMessage(code), this.getRequest(req));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode());
return new ResponseEntity<>(response, httpHeaders, httpStatus);
}
// 获取请求方法和路径
private String getRequest(HttpServletRequest req) {
String url = req.getRequestURI();
String method = req.getMethod();
return method + " " + url;
}
}
自定义HttpException的处理比较特殊,因为不同的 HttpException 要返回不同的 HttpStatusCode,所有需要使用ResponseEntity<UnifyResponse>进行返回。
如上文所示,我们需要添加@RestControllerAdvice和@ExceptionHandler(HttpException.class)注解,其次我们需要分别定义UnifyResponse,httpHeaders和httpStatus,最终将结果包装进ResponseEntity进行返回。
# 参数校验异常
参数校验分为两类,一类是PathVariables,RequesetParams的校验,返回ConstraintViolationException;另一类是RequestBody的校验,返回MethodArgumentNotValidException。
我们需要分别定义这两类异常的异常处理方法并获取信息返回UnifyResponse。
@RestControllerAdvice
public class GlobalExceptionAdvice {
@Autowired
private ExceptionCodeConfiguration codeConfiguration;
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyResponse handleConstraintException(HttpServletRequest req, ConstraintViolationException e) {
StringBuilder messageBuilder = new StringBuilder();
e.getConstraintViolations().forEach(violation -> {
String param = violation.getPropertyPath().toString().replaceAll("\\w+\\.", "");
String message = violation.getMessage();
messageBuilder.append(param).append(":").append(message).append(";");
});
return new UnifyResponse(ExceptionCode.PARAMS_ERROR, messageBuilder.toString(), this.getRequest(req));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public UnifyResponse handleMethodArgumentException(HttpServletRequest req, MethodArgumentNotValidException e) {
StringBuilder messageBuilder = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(fieldError -> {
String field = fieldError.getField();
String message = fieldError.getDefaultMessage();
messageBuilder.append(field).append(":").append(message).append(";");
});
return new UnifyResponse(ExceptionCode.PARAMS_ERROR, messageBuilder.toString(), this.getRequest(req));
}
// 获取请求方法和路径
private String getRequest(HttpServletRequest req) {
String url = req.getRequestURI();
String method = req.getMethod();
return method + " " + url;
}
}
ConstraintViolationException通过getConstraintViolations()返回Set<ConstraintViolation<?>>集合,我们使用 forEach 和 StringBuilder 遍历和收集相应的信息。
ConstraintViolation的getPropertyPath().toString()会返回MethodName.Param格式,我们不希望给出MethodName,于是使用replaceAll()将其替换为空。getMessage()获得该 Param 的校验失败信息。
MethodArgumentNotValidException通过getFieldErrors()获得List<FieldError>列表,我们同样使用 forEach 和 StringBuilder 遍历和收集信息。
FieldError的getField(),getDefaultMessage()获得 DTO 对象内相应的字段和校验失败信息。
最终我们将信息打包成UnifyResponse进行返回。
源代码:https://github.com/PeterWangYong/blog-code/tree/master/handle-exception
← SpringBoot 数据校验 →