SpringBoot 全局异常处理

本文介绍使用@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接收HttpServletRequestException两个参数类型,用于接收数据进行处理。

最终,对数据进行处理并打包成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)注解,其次我们需要分别定义UnifyResponsehttpHeadershttpStatus,最终将结果包装进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遍历和收集相应的信息。

ConstraintViolationgetPropertyPath().toString()会返回MethodName.Param格式,我们不希望给出MethodName,于是使用replaceAll()将其替换为空。getMessage()获得该Param的校验失败信息。

MethodArgumentNotValidException通过getFieldErrors()获得List<FieldError>列表,我们同样使用forEach和StringBuilder遍历和收集信息。

FieldErrorgetField(),getDefaultMessage()获得DTO对象内相应的字段和校验失败信息。

最终我们将信息打包成UnifyResponse进行返回。

源代码:https://github.com/PeterWangYong/blog-code/tree/master/handle-exception

posted @ 2020-05-06 10:42  Peterer~王勇  阅读(522)  评论(0编辑  收藏  举报