全局异常处理及参数校验(全局异常处理注解)

  本篇文章为你整理了全局异常处理及参数校验(全局异常处理注解)的详细内容,包含有全局异常处理@controlleradvice 全局异常处理注解 全局异常怎么处理 全局异常处理@controlleradvice捕捉@notnull 全局异常处理及参数校验,希望能帮助你了解 全局异常处理及参数校验。

  程序员优雅哥 (youyacoder)

  程序员优雅哥 (公众号同名,第一时间更新)

  十年程序员,呆过央企外企私企,做过前端后端架构。分享vue、Java等前后端技术和架构

  
 

   前后端分离开发非常普遍,后端处理业务,为前端提供接口。服务中总会出现很多运行时异常和业务异常,本文主要讲解在 SpringBoot 实战中如何进行异常统一处理和请求参数的校验。

  
优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验

  前后端分离开发非常普遍,后端处理业务,为前端提供接口。服务中总会出现很多运行时异常和业务异常,本文主要讲解在 SpringBoot 实战中如何进行异常统一处理和请求参数的校验。

  1 异常统一处理

  所有异常处理相关的类,咱们都放到 com.yygnb.demo.common包中。

  当后端发生异常时,需要按照一个约定的规则(结构)返回给前端,所以先定义一个发生异常时固定的结构。

  1.1 错误响应结构

  发生异常时的响应结构约定两个字段:code——错误编码;msg——错误消息。创建类:

  com.yygnb.demo.common.domain.ErrorResult:

  

@Data

 

  @Builder

  @NoArgsConstructor

  @AllArgsConstructor

  public class ErrorResult implements Serializable {

   private static final long serialVersionUID = -8738363760223125457L;

   * 错误码

   private String code;

   * 错误消息

   private String msg;

   public static ErrorResult build(ErrorResult commonErrorResult, String msg) {

   return new ErrorResult(commonErrorResult.getCode(), commonErrorResult.getMsg() + " " + msg);

  

 

  1.2 通用错误响应常量

  有些异常返回的 ErrorResult 是一样的,如参数校验错误、未查询到对象、系统异常等,可以定义一些错误响应的常量:

  com.yygnb.demo.common.exception.DefaultErrorResult:

  

public interface DefaultErrorResult {

 

   ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系统异常");

   ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定义异常");

   ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "参数绑定错误:");

   ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "参数校验错误:");

   ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON转换异常");

   ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根据编码没有查询到对象");

   ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根据ID没有查询到对象");

  

 

  1.3 通用异常类定义

  定义一个通用的异常类 CommonException,继承自 RuntimeException,当程序中捕获到编译时异常或业务异常时,就抛出这个通用异常,交给全局来处理。(随着业务复杂度的增加,可以细分自定义异常,如 AuthException、UserException、CouponException 等,让这些细分异常都继承自 CommonException。)

  com.yygnb.demo.common.exception.CommonException:

  

@EqualsAndHashCode(callSuper = true)

 

  @Data

  public class CommonException extends RuntimeException {

   protected ErrorResult errorResult;

   public CommonException(String message) {

   super(message);

   public CommonException(String message, Throwable cause) {

   super(message, cause);

   public CommonException(Throwable cause) {

   super(cause);

   protected CommonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {

   super(message, cause, enableSuppression, writableStackTrace);

   public CommonException(String code, String msg) {

   super(msg + "(" + code + ")");

   this.errorResult = new ErrorResult(code, msg);

   public CommonException(ErrorResult errorResult) {

   super(errorResult.getMsg() + "(" + errorResult.getCode() + ")");

   this.errorResult = errorResult;

  

 

  这个自定义异常类复写了父类的构造函数,同时定义了一个成员变量 ErrorResult,便于在同一异常处理时快速构造返回信息。

  1.4 全局异常处理

  Spring MVC 中提供了全局异常处理的注解:@ControllerAdvice 和 @RestControllerAdvice。由于前后端分离开发 RESTful 接口,我们这里就使用 @RestControllerAdvice。

  com.yygnb.demo.common.exception.CommonExceptionHandler:

  

@Slf4j

 

  @RestControllerAdvice

  public class CommonExceptionHandler {

   * 通用业务异常

   * @param e

   * @return

   @ExceptionHandler(value = CommonException.class)

   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

   public ErrorResult handleCommonException(CommonException e) {

   log.error("{}, {}", e.getMessage(), e);

   if (e.getErrorResult() != null) {

   return e.getErrorResult();

   return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());

   * 其他运行时异常

   * @param e

   * @return

   @ExceptionHandler(value = Exception.class)

   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

   public ErrorResult handleDefaultException(Exception e) {

   log.error("{}, {}", e.getMessage(), e);

   return DefaultErrorResult.SYSTEM_ERROR;

  

 

  上面捕获了 CommonException 和 Exception,匹配顺序为从上往下。假设 handleDefaultException 在前面,当发生一个异常 CommonException 时,一来就会被 handleDefaultException 捕获,因为无论什么异常,都属于 Exception 的实例,不会执行 handleCommonException。所以越具体的异常处理,越要写在前面。

  1.5 测试统一异常处理

  在 DemoController 中抛出一个 CommonException:

  

@GetMapping("hello")

 

  public String hello(String msg) {

   String result = "Hello Spring Boot ! " + msg;

   if ("demo".equals(msg)) {

   throw new CommonException("发生错误----这是自定义异常");

   return result;

  

 

  启动服务,访问:

  

http://localhost:9099/demo/hello?msg=demo

 

  

 

  结果返回:

  

{

 

   "code": "C99999",

   "msg": "发生错误----这是自定义异常"

  

 

  可以看出全局统一异常处理已经生效了。

  2 参数校验

  传统参数校验方式是通过多个 if/else 来进行,代码量大,很没有意义。Spring Boot 中有个 starter spring-boot-starter-validation 可以帮助咱们很方便的实现参数校验。

  2.1 添加依赖

  有些文章中说 spring boot 2.3 还是多少版本以后不用手动加入这个 starter,我试了以后不行,需要手动引入该依赖才行。

  

 dependency 

 

   groupId org.springframework.boot /groupId

   artifactId spring-boot-starter-validation /artifactId

   /dependency

  

 

  这个 starter 定义 Validator 以及 SmartValidator 接口,提供 @Validated,支持 spring 环境,支持验证组的规范, 提供了一系列的工厂类以及适配器。底层依赖 hibernate-validator 包。

  2.2 完善异常处理类

  在 1.4 中只捕获了 CommonException 和 Exception,此处要完善参数绑定、校验等异常。补充后 CommonExceptionHandler 如下:

  

@Slf4j

 

  @RestControllerAdvice

  public class CommonExceptionHandler {

   @ExceptionHandler(value = BindException.class)

   @ResponseStatus(HttpStatus.BAD_REQUEST)

   public ErrorResult handleBindException(BindException e) {

   log.error("{}", e.getMessage(), e);

   List String defaultMsg = e.getBindingResult().getAllErrors()

   .stream()

   .map(ObjectError::getDefaultMessage)

   .collect(Collectors.toList());

   return ErrorResult.build(DefaultErrorResult.PARAM_BIND_ERROR, defaultMsg.get(0));

   @ExceptionHandler(value = MethodArgumentNotValidException.class)

   @ResponseStatus(HttpStatus.BAD_REQUEST)

   public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {

   log.error("{}", e.getMessage(), e);

   List String defaultMsg = e.getBindingResult().getFieldErrors()

   .stream()

   .map(fieldError - "【" + fieldError.getField() + "】" + fieldError.getDefaultMessage())

   .collect(Collectors.toList());

   return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));

   @ExceptionHandler(value = MissingServletRequestParameterException.class)

   @ResponseStatus(HttpStatus.BAD_REQUEST)

   public ErrorResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {

   log.error("{}", e.getMessage(), e);

   log.error("ParameterName: {}", e.getParameterName());

   log.error("ParameterType: {}", e.getParameterType());

   return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, e.getMessage());

   @ExceptionHandler(value = ConstraintViolationException.class)

   @ResponseStatus(HttpStatus.BAD_REQUEST)

   public ErrorResult handleBindGetException(ConstraintViolationException e) {

   log.error("{}", e.getMessage(), e);

   List String defaultMsg = e.getConstraintViolations()

   .stream()

   .map(ConstraintViolation::getMessage)

   .collect(Collectors.toList());

   return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));

   @ExceptionHandler(HttpMessageNotReadableException.class)

   @ResponseStatus(HttpStatus.BAD_REQUEST)

   public ErrorResult error(HttpMessageNotReadableException e){

   log.error("{}", e.getMessage(), e);

   return DefaultErrorResult.JSON_PARSE_ERROR;

   * 通用业务异常

   * @param e

   * @return

   @ExceptionHandler(value = CommonException.class)

   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

   public ErrorResult handleCommonException(CommonException e) {

   log.error("{}, {}", e.getMessage(), e);

   if (e.getErrorResult() != null) {

   return e.getErrorResult();

   return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());

   * 其他运行时异常

   * @param e

   * @return

   @ExceptionHandler(value = Exception.class)

   @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

   public ErrorResult handleDefaultException(Exception e) {

   log.error("{}, {}", e.getMessage(), e);

   return DefaultErrorResult.SYSTEM_ERROR;

  

 

  2.3 自定义校验注解

  在 javax.validation 中提供了很多用于校验的注解,常见的如:@NotNull、@Min、@Max 等等,但可能这些注解不够,需要自定义注解。例如咱们自定义一个注解 @OneOf,该注解对应字段的值只能从 value 中选择:使用方式为:

  

@OneOf(value = {"MacOS", "Windows", "Linux"})

 

  

 

  首先定义一个注解

  com.yygnb.demo.common.validator.OneOf:

  

@Target(ElementType.FIELD)

 

  @Retention(RetentionPolicy.RUNTIME)

  @Documented

  @Constraint(validatedBy = OneOfValidator.class)

  public @interface OneOf {

   String message() default "只能从备选值中选择";

   Class ? [] groups() default {};

   Class ? extends Payload [] payload() default {};

   String[] value();

  

 

  定义注解时,@Constraint(validatedBy = OneOfValidator.class)表示校验使用 OneOfValidator 类进行校验,我们需要编写这个类。

  com.yygnb.demo.common.validator.OneOfValidator:

  

public class OneOfValidator implements ConstraintValidator OneOf, String {

 

   private List String list;

   @Override

   public void initialize(OneOf constraintAnnotation) {

   list = Arrays.asList(constraintAnnotation.value());

   @Override

   public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {

   if (s == null) {

   return true;

   return list.contains(s);

  

 

  这样便实现一个自定义校验注解了。

  2.4 RequestBody 参数校验

  以新增电脑接口为例,首先需要在方法的参数前面使用注解 @Valid 或 @Validated 修饰。在此处使用两者之一都可以,前者是 javax.validation 中提供的,后者是 springframework 中提供的:

  

@Operation(summary = "新增电脑")

 

  @PostMapping()

  public Computer save(@RequestBody @Validated Computer computer) {

   computer.setId(null);

   this.computerService.save(computer);

   return computer;

  

 

  在 RequestBody 参数对应的 DTO / 实体中,对需要校验的字段加上校验注解。例如 操作系统operation 只能从 "MacOS", "Windows", "Linux" 中选择;年份year不能为空且长度为4:

  

@OneOf(value = {"MacOS", "Windows", "Linux"})

 

  @Schema(title = "操作系统")

  private String operation;

  @NotNull(message = "不能为空")

  @Length(min = 4, max = 4)

  @Schema(title = "年份")

  private String year;

  

 

  此时重启服务,调用新增电脑接口时,就会进行校验。

  2.5 路径参数和RequestParam校验

  路径参数和没有封装为实体的 RequestParam 参数,首先需要在参数前面加上校验注解,然后需要在 Controller 类上面加上注解 @Validated 才会生效。如在分页列表接口中,要求参数当前页 page 大于 0:

  

public Page Computer findPage(@PathVariable @Min(1) Integer page, @PathVariable @Max(10) Integer size) {

 

  

 

  本文简单介绍了统一异常处理和参数校验,本节的代码还有很多优化空间,在后面的实战部分逐一完善。

  
 

  \/ 程序员优雅哥(youyacoder),今日学习到此结束~~~

  以上就是全局异常处理及参数校验(全局异常处理注解)的详细内容,想要了解更多 全局异常处理及参数校验的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: