SpringBoot-表單驗證-統一異常處理-自定義驗證資訊源

張鐵牛發表於2021-08-08

1. 簡介

我們都知道前臺的驗證只是為了滿足介面的友好性、客戶體驗性等等。但是如果僅靠前端進行資料合法性校驗,是遠遠不夠的。因為非法使用者可能會直接從客戶端獲取到請求地址進行非法請求,所以後臺的校驗是必須的;特別是應用如果不允許輸入空值,對資料的合法行有要求的情況下。

2. 開擼

2.1 專案結構

結構說明:

├── java
│   └── com
│       └── ldx
│           └── valid
│               ├── ValidApplication.java # 啟動類
│               ├── annotation
│               │   └── Phone.java # 自定義驗證註解
│               ├── config
│               │   └── ValidatorConfig.java # 表單驗證配置類
│               ├── controller
│               │   └── SysUserController.java # 使用者管理控制器
│               ├── exception
│               │   ├── BusinessException.java # 業務異常類
│               │   └── GlobalExceptionHandler.java # 統一異常處理類
│               ├── model
│               │   ├── SysUser.java # 使用者資訊實體
│               │   └── ValidationInterface.java # 表單驗證的通用分組介面
│               ├── util
│               │   └── CommonResult.java # 介面返回封裝類
│               └── validation
│                   └── PhoneValidator.java #自定義驗證實現類
└── resources
    ├── application.yaml # 配置檔案
    └── messages
        └── validation
            └── messages.properties # 自定義驗證資訊源

2.1 quick start

2.1.1 匯入依賴

建立springboot專案匯入以下依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ldx</groupId>
    <artifactId>valid</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>valid</name>
    <description>表單驗證demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- web支援 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 表單驗證 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2.1.2 新增配置類

建立表單驗證配置類,配置快速校驗,不用等全部的引數校驗完,只要有錯,馬上丟擲。

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

/**
 * 配置 Hibernate 引數校驗
 * @author ludangxin
 * @date 2021/8/5
 */
@Configuration
public class ValidatorConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        //快速校驗,只要有錯馬上返回
        postProcessor.setValidator(validator());
        return postProcessor;
    }

    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            .addProperty("hibernate.validator.fail_fast", "true")
            .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

2.1.3 新增實體類

import lombok.*;
import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 使用者資訊管理
 * @author ludangxin
 * @date 2021/8/5
 */
@Data
public class SysUser  implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主鍵
     */
    private Long id;

    /**
     * 使用者名稱
     */
    @NotEmpty(message = "使用者名稱稱不能為空")
    private String username;

    /**
     * 密碼
     */
    @Size(min = 6, max = 16, message = "密碼長度必須在{min}-{max}之間")
    private String password = "123456";

    /**
     * 郵箱地址
     */
    @Email(message = "郵箱地址不合法")
    @NotEmpty(message = "郵箱不能為空")
    private String email;

    /**
     * 電話
     */
    @Size(min = 11, max = 11, message = "手機號不合法")
    @NotEmpty(message = "手機號不能為空")
    private String phone;
}

2.1.4 介面返回封裝類

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 操作訊息提醒
 * @author ludangxin
 * @date 2021/8/5
 */
@Data
@NoArgsConstructor
public class CommonResult {
    /** 狀態碼 */
    private int code;

    /** 返回內容 */
    private String msg;

    /** 資料物件 */
    private Object data;

    /**
     * 初始化一個新建立的 CommonResult 物件
     * @param type 狀態型別
     * @param msg 返回內容
     */
    public CommonResult(Type type, String msg) {
        this.code = type.value;
        this.msg = msg;
    }

    /**
     * 初始化一個新建立的 CommonResult 物件
     * @param type 狀態型別
     * @param msg 返回內容
     * @param data 資料物件
     */
    public CommonResult(Type type, String msg, Object data) {
        this.code = type.value;
        this.msg = msg;
        if (data != null) {
            this.data = data;
        }
    }

    /**
     * 返回成功訊息
     * @return 成功訊息
     */
    public static CommonResult success() {
        return CommonResult.success("操作成功");
    }

    /**
     * 返回成功資料
     * @return 成功訊息
     */
    public static CommonResult success(Object data) {
        return CommonResult.success("操作成功", data);
    }

    /**
     * 返回成功訊息
     * @param msg 返回內容
     * @return 成功訊息
     */
    public static CommonResult success(String msg) {
        return CommonResult.success(msg, null);
    }

    /**
     * 返回成功訊息
     * @param msg 返回內容
     * @param data 資料物件
     * @return 成功訊息
     */
    public static CommonResult success(String msg, Object data) {
        return new CommonResult(Type.SUCCESS, msg, data);
    }

    /**
     * 返回警告訊息
     * @param msg 返回內容
     * @return 警告訊息
     */
    public static CommonResult warn(String msg) {
        return CommonResult.warn(msg, null);
    }

    /**
     * 返回警告訊息
     * @param msg 返回內容
     * @param data 資料物件
     * @return 警告訊息
     */
    public static CommonResult warn(String msg, Object data) {
        return new CommonResult(Type.WARN, msg, data);
    }

    /**
     * 返回錯誤訊息
     * @return 錯誤資訊
     */
    public static CommonResult error() {
        return CommonResult.error("操作失敗");
    }

    /**
     * 返回錯誤訊息
     * @param msg 返回內容
     * @return 錯誤訊息
     */
    public static CommonResult error(String msg) {
        return CommonResult.error(msg, null);
    }

    /**
     * 返回錯誤訊息
     * @param msg 返回內容
     * @param data 資料物件
     * @return 錯誤訊息
     */
    public static CommonResult error(String msg, Object data) {
        return new CommonResult(Type.ERROR, msg, data);
    }

    /**
     * 狀態型別
     */
    public enum Type {
        /** 成功 */
        SUCCESS(200),
        /** 警告 */
        WARN(301),
        /** 錯誤 */
        ERROR(500);
        private final int value;

        Type(int value){
            this.value = value;
        }

        public int value() {
            return this.value;
        }
    }
}

2.1.5 控制器

使用@Validated註解標識需要驗證的類,使用BindingResult類接收錯誤資訊

import com.ldx.valid.exception.BusinessException;
import com.ldx.valid.model.SysUser;
import com.ldx.valid.model.ValidationInterface;
import com.ldx.valid.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 使用者管理控制器
 *
 * @author ludangxin
 * @date 2021/8/5
 */
@Slf4j
@RestController
@RequestMapping("sys/user")
public class SysUserController {
   private static final List<SysUser> USERS = new ArrayList<>();

   // 資料初始化
   static {
      SysUser user = new SysUser();
      user.setId(1L);
      user.setUsername("zhangsan");
      user.setPhone("13566666666");
      user.setEmail("example@qq.com");
      USERS.add(user);
      SysUser user1 = new SysUser();
      user1.setId(2L);
      user1.setUsername("lisi");
      user1.setPhone("13588888888");
      user1.setEmail("example1@qq.com");
      USERS.add(user1);
   }

   /**
    * 新增使用者資訊
    * @param sysUser 使用者資訊
    * @return 成功標識
    */
   @PostMapping
   public CommonResult add(@Validated @RequestBody SysUser sysUser, BindingResult result) {
      FieldError fieldError = result.getFieldError();

      if(Objects.nonNull(fieldError)) {
         String field = fieldError.getField();
         Object rejectedValue = fieldError.getRejectedValue();
         String msg = "[" + fieldError.getDefaultMessage() + "]";
         log.error("{}:欄位=={}\t值=={}", msg, field, rejectedValue);
         return CommonResult.error(msg);
      }

      USERS.add(sysUser);
      return CommonResult.success("新增成功");
   }
}

2.1.5 啟動測試

新增時,故意將email資訊填錯,測試結果符合預期。

log日誌:

[nio-8080-exec-9] c.l.valid.controller.SysUserController   : [郵箱地址不合法]:欄位==email	值==123

3. 分組校驗

groups是用來幹什麼的?

因為一個實體不可能只幹一種操作,一個實體必然存在增刪改查操作,那麼問題就來了
如果我要根據id進行更新操作,那麼id肯定不能為空
這時候我還要進行新增操作,因為id是新增資料庫操作才產生的,接受資料的時候我肯定是沒有id的
所以就產生矛盾了
那麼groups這個引數就起作用了,它可以表示我這個註解屬於哪個組,這樣就解決這個尷尬的問題了。

當在controller中校驗表單資料時,如果使用了groups,那麼沒有在這個分組下的屬性是不會校驗的

3.1 新增分組介面

/**
 * 用於表單驗證的通用分組介面
 * @author ludangxin
 * @date 2021/8/5
 */
public interface ValidationInterface {
    /**
     * 新增分組
     */
    interface add{}

    /**
     * 刪除分組
     */
    interface delete{}

    /**
     * 查詢分組
     */
    interface select{}

    /**
     * 更新分組
     */
    interface update{}
}

如果還有其它特殊的分組要求 直接在DO中建立interface即可
例:如果還有個需要驗證username 和 password(只有這兩個引數) 的 select操作
直接在SysUser中建立UsernamePasswordValidView 的介面即可

3.2 修改實體類

將屬性進行分組

import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.io.Serializable;

/**
 * 使用者資訊管理
 * @author ludangxin
 * @date 2021/8/5
 */
@Data
public class SysUser implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主鍵
     */
    @NotNull(message = "id不能為空", groups = {ValidationInterface.update.class})
    private Long id;

    /**
     * 使用者名稱
     */
    @NotEmpty(message = "使用者名稱稱不能為空", groups = {
              ValidationInterface.update.class, 
              ValidationInterface.add.class})
    private String username;

    /**
     * 密碼
     */
    @Size(min = 6, max = 16, message = "密碼長度必須在{min}-{max}之間", groups = {
          ValidationInterface.update.class, 
          ValidationInterface.add.class})
    private String password = "123456";

    /**
     * 郵箱地址
     */
    @Email(message = "郵箱地址不合法", groups = {
           ValidationInterface.update.class, 
           ValidationInterface.add.class, 
           ValidationInterface.select.class})
    @NotEmpty(message = "郵箱不能為空", groups = ValidationInterface.add.class)
    private String email;

    /**
     * 電話
     */
    @Size(min = 11, max = 11, message = "手機號不合法", groups = {
          ValidationInterface.update.class,
          ValidationInterface.add.class,
          ValidationInterface.select.class})
    @NotEmpty(message = "手機號不能為空",groups = {ValidationInterface.add.class})
    private String phone;
}

3.3 修改控制器

新增操作方法,並且方法形參上指定驗證的分組

import com.ldx.valid.exception.BusinessException;
import com.ldx.valid.model.SysUser;
import com.ldx.valid.model.ValidationInterface;
import com.ldx.valid.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 使用者管理控制器
 * @author ludangxin
 * @date 2021/8/5
 */
@Slf4j
@RestController
@RequestMapping("sys/user")
public class SysUserController {
   private static final List<SysUser> USERS = new ArrayList<>();

   // 資料初始化
   static {
      SysUser user = new SysUser();
      user.setId(1L);
      user.setUsername("zhangsan");
      user.setPhone("13566666666");
      user.setEmail("example@qq.com");
      USERS.add(user);
      SysUser user1 = new SysUser();
      user1.setId(2L);
      user1.setUsername("lisi");
      user1.setPhone("13588888888");
      user1.setEmail("example1@qq.com");
      USERS.add(user1);
   }

   /**
    * 根據手機號或郵箱查詢使用者資訊
    * @param sysUser 查詢條件
    * @return 使用者list
    */
   @GetMapping
   public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser,
                                 BindingResult result)
   {
      FieldError fieldError = result.getFieldError();

      if(Objects.nonNull(fieldError)) {
         return CommonResult.error(getErrorMsg(fieldError));
      }

      String phone = sysUser.getPhone();
      String email = sysUser.getEmail();

      if(phone == null && email == null) {
         return CommonResult.success(USERS);
      }

      List<SysUser> queryResult = USERS.stream()
            .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
            .collect(Collectors.toList());
      return CommonResult.success(queryResult);
   }

   /**
    * 新增使用者資訊
    * @param sysUser 使用者資訊
    * @return 成功標識
    */
   @PostMapping
   public CommonResult add(@Validated(value = ValidationInterface.add.class)
                           @RequestBody SysUser sysUser,
                           BindingResult result)
   {
      FieldError fieldError = result.getFieldError();

      if(Objects.nonNull(fieldError)) {
         return CommonResult.error(getErrorMsg(fieldError));
      }
     
      Long id = (long) (USERS.size() + 1);
      sysUser.setId(id);
      USERS.add(sysUser);
      return CommonResult.success("新增成功");
   }

   /**
    * 根據Id更新使用者資訊
    * @param sysUser 使用者資訊
    * @return 成功標識
    */
   @PutMapping("{id}")
   public CommonResult updateById(@PathVariable("id") Long id,
                                  @Validated(value = ValidationInterface.update.class)
                                  @RequestBody SysUser sysUser,
                                  BindingResult result)
   {
      FieldError fieldError = result.getFieldError();

      if(Objects.nonNull(fieldError)) {
         return CommonResult.error(getErrorMsg(fieldError));
      }

      for(int i = 0; i < USERS.size(); i++) {
         if(USERS.get(i).getId().equals(id)) {
            USERS.set(i,sysUser);
         }
      }
      return CommonResult.success("更新成功");
   }

   /**
    * 根據Id刪除使用者資訊
    * @param id 主鍵
    * @return 成功標識
    */
   @DeleteMapping("{id}")
   public CommonResult deleteById(@PathVariable Long id) {
      USERS.removeIf(obj -> obj.getId().equals(id));
      return CommonResult.success("刪除成功");
   }

   /**
    * 獲取表單驗證錯誤msg
    * @param fieldError 報錯欄位
    * @return msg
    */
   public String getErrorMsg(FieldError fieldError) {
      String field = fieldError.getField();
      Object rejectedValue = fieldError.getRejectedValue();
      String msg = "[" + fieldError.getDefaultMessage() + "]";
      log.error("{}:欄位=={}\t值=={}", msg, field, rejectedValue);
      return msg;
   }
}

3.4 啟動測試

查詢:

​ 輸入不合法手機號

新增:

​ 正常情況

​ 去掉郵箱

修改:

​ 去掉id

刪除:

4. 自定義驗證

很多時候框架提供的功能並不能滿足我們的業務場景,這時我們需要自定義一些驗證規則來完成驗證。

4.1 新增註解

import com.ldx.valid.validation.PhoneValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 驗證手機號是否合法
 * @author ludangxin
 * @date 2021/8/7
 */
@Documented
@Constraint(validatedBy = {PhoneValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface Phone {
   //預設錯誤訊息
   String message() default "不是一個合法的手機號";

   //分組
   Class<?>[] groups() default {};

   //載荷 將某些後設資料資訊與給定的註解宣告相關聯的方法
   Class<? extends Payload>[] payload() default {};

   //指定多個時使用
   @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
   @Retention(RUNTIME)
   @Documented
   @interface List {
      Phone[] value();
   }
}

4.2 編寫驗證邏輯

import javax.validation.ConstraintValidator;
import com.ldx.valid.annotation.Phone;
import javax.validation.ConstraintValidatorContext;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * 手機號校驗器
 * @author ludangxin
 * @date 2021/8/7
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {

   /**
    * 手機號正規表示式
    */
   private static final String REGEXP_PHONE = "^1[3456789]\\d{9}$";

   @Override
   public boolean isValid(String value, ConstraintValidatorContext context) {
      if(Objects.isNull(value)) {
         return true;
      }

      return Pattern.matches(REGEXP_PHONE, value);
   }
}

4.3 修改實體

SysUser.phone 屬性新增註解@Phone

@Phone(groups = {
                ValidationInterface.update.class,
                ValidationInterface.add.class,
                ValidationInterface.select.class})
@NotEmpty(message = "手機號不能為空", groups = {ValidationInterface.add.class})
private String phone;

4.4 啟動測試

輸入錯誤的手機號進行測試

4.5 @Pattern

當然validation也提供了基於正則匹配的註解@Pattern

@Pattern(message = "手機號不合法", regexp = "^1[3456789]\\d{9}$", groups = {ValidationInterface.add.class})
@NotEmpty(message = "手機號不能為空", groups = {ValidationInterface.add.class})
private String phone;

注意是javax.validation.constraints包下的

測試

5. 呼叫過程驗證

有的時候我們在引數傳輸過程中需要對傳入的物件做引數驗證,但是上面介紹的都是對引數繫結時的驗證,那能不能使用validation進行驗證呢?

答案肯定是可以的。

5.1 使用 spring bean

5.1.1 注入validator

bean validator 是我們在config檔案中定義的bean,如果使用了springboot預設的配置ValidationAutoConfiguration::defaultValidator(),直接注入bean name defaultValidator即可

@Resource(name = "validator")
javax.validation.Validator validator;

5.1.2 定義驗證方法

public void validateParams(SysUser user) {
   validator.validate(user, ValidationInterface.add.class).stream().findFirst().ifPresent(obj -> {
      String objName = obj.getRootBean().getClass().getSimpleName();
      String fieldName = obj.getPropertyPath().toString();
      Object val = obj.getInvalidValue();
      String msg = obj.getMessage();
      String errMsg = MessageFormat.format(msg + ":物件:{0},欄位:{1},值:{2}", objName, fieldName, val);
      throw new RuntimeException(errMsg);
   });

5.1.2 啟動驗證

呼叫新增方法,通過新增方法呼叫validateParams方法

報錯日誌如下

java.lang.RuntimeException: 手機號不合法:物件:SysUser,欄位:phone,值:135999

5.2 非spring環境驗證

5.2.1 定義驗證方法

直接獲取預設的工廠類,然後獲取驗證物件進行驗證

public static void main(String[] args) {
   ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
   Validator validator = vf.getValidator();
   SysUser user = new SysUser();
   user.setId(1L);
   user.setUsername("zhangsan");
   user.setPhone("1356666");
   user.setEmail("example@qq.com");
   validator.validate(user, ValidationInterface.add.class).stream().findFirst().ifPresent(obj -> {
      String objName = obj.getRootBean().getClass().getSimpleName();
      String fieldName = obj.getPropertyPath().toString();
      Object val = obj.getInvalidValue();
      String msg = obj.getMessage();
      String errMsg = MessageFormat.format(msg + ":物件:{0},欄位:{1},值:{2}", objName, fieldName, val);
      throw new RuntimeException(errMsg);
   });
}

5.2.2 啟動驗證

報錯資訊如下,符合預期

Exception in thread "main" java.lang.RuntimeException: 手機號不合法:物件:SysUser,欄位:phone,值:1356666
	at com.ldx.valid.controller.SysUserController.lambda$main$4(SysUserController.java:215)
	at java.util.Optional.ifPresent(Optional.java:159)
	at com.ldx.valid.controller.SysUserController.main(SysUserController.java:209)

6. 方法引數驗證

有的時候我們想在方法上直接進行引數驗證,步驟如下

6.1 修改控制器

直接在類上新增註解@Validated,並在方法上直接進行驗證

@Slf4j
@Validated
@RestController
@RequestMapping("sys/user")
public class SysUserController {
  ... 省略程式碼
    /**
     * 根據手機號和郵箱查詢使用者資訊
     * @param phone 手機號
     * @return 使用者list
     */
    @GetMapping("selectByPhone")
    public CommonResult queryByPhone(@NotEmpty(message = "手機號不能為空") String phone) {
       List<SysUser> queryResult = USERS.stream()
             .filter(obj -> obj.getPhone().equals(phone))
             .collect(Collectors.toList());
       return CommonResult.success(queryResult);
    }
}

6.2 啟動驗證

不給phone欄位賦值,操作結果符合預期

錯誤日誌:

javax.validation.ConstraintViolationException: queryByPhone.phone: 手機號不能為空

7. 統一異常處理

在上面的引數驗證中,驗證的錯誤資訊是通過BindingResult result引數進行接收的,在每個方法中異常處理如出一轍,特別麻煩。甚至在step 5,6都是直接將異常的堆疊資訊返回給前端,這對於用來說是非常不友好的。而且有的情況下需要我們主動丟擲業務異常,比方使用者不能直接刪除已繫結使用者的角色。

所以,開擼。

7.1 建立業務異常類

/**
 * 業務異常
 * @author ludangxin
 * @date 2021/8/5
 */
public class BusinessException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    protected final String message;

    public BusinessException(String message) {
        this.message = message;
    }

    public BusinessException(String message, Throwable e) {
        super(message, e);
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

7.2 建立全域性異常處理器

import com.ldx.valid.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.Set;

/**
 * 全域性異常處理器
 *
 * @author ludangxin
 * @date 2021/8/5
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 引數繫結異常類 用於表單驗證時丟擲的異常處理
     */
    @ExceptionHandler(BindException.class)
    public CommonResult validatedBindException(BindException e){
        log.error(e.getMessage(), e);
        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = e.getFieldError();
        String message = "[" + e.getAllErrors().get(0).getDefaultMessage() + "]";
        return CommonResult.error(message);
    }

    /**
     * 用於方法形參中引數校驗時丟擲的異常處理
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public CommonResult handle(ValidationException e) {
        log.error(e.getMessage(), e);
        String errorInfo = "";
        if(e instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) e;
            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            for (ConstraintViolation<?> item : violations) {
                errorInfo = errorInfo + "[" + item.getMessage() + "]";
            }
        }
        return CommonResult.error(errorInfo);
    }

    /**
     * 請求方式不支援
     */
    @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
    public CommonResult handleException(HttpRequestMethodNotSupportedException e){
        log.error(e.getMessage(), e);
        return CommonResult.error("不支援' " + e.getMethod() + "'請求");
    }

    /**
     * 攔截未知的執行時異常
     */
    @ExceptionHandler(RuntimeException.class)
    public CommonResult notFount(RuntimeException e) {
        log.error("執行時異常:", e);
        return CommonResult.error("執行時異常:" + e.getMessage());
    }

    /**
     * 系統異常
     */
    @ExceptionHandler(Exception.class)
    public CommonResult handleException(Exception e) {
        log.error(e.getMessage(), e);
        return CommonResult.error("伺服器錯誤,請聯絡管理員");
    }

    /**
     * 業務異常
     */
    @ExceptionHandler(BusinessException.class)
    public CommonResult businessException(HttpServletRequest request, BusinessException e) {
        log.error(e.getMessage());
        return CommonResult.error(e.getMessage());
    }
}

7.3 修改控制器

刪除方法中的BindingResult result引數,將錯誤直接拋給統一異常處理類去解決即可。

import com.ldx.valid.exception.BusinessException;
import com.ldx.valid.model.SysUser;
import com.ldx.valid.model.ValidationInterface;
import com.ldx.valid.service.UserService;
import com.ldx.valid.util.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.*;
import javax.validation.constraints.NotEmpty;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 使用者管理控制器
 * @author ludangxin
 * @date 2021/8/5
 */
@Slf4j
@Validated
@RestController
@RequestMapping("sys/user")
public class SysUserController {
   private static final List<SysUser> USERS = new ArrayList<>();

   // 資料初始化
   static {
      SysUser user = new SysUser();
      user.setId(1L);
      user.setUsername("zhangsan");
      user.setPhone("13566666666");
      user.setEmail("example@qq.com");
      USERS.add(user);
      SysUser user1 = new SysUser();
      user1.setId(2L);
      user1.setUsername("lisi");
      user1.setPhone("13588888888");
      user1.setEmail("example1@qq.com");
      USERS.add(user1);
   }

   /**
    * 根據手機號或郵箱查詢使用者資訊
    * @param sysUser 查詢條件
    * @return 使用者list
    */
   @GetMapping
   public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser) {
      String phone = sysUser.getPhone();
      String email = sysUser.getEmail();

      if(phone == null && email == null) {
         return CommonResult.success(USERS);
      }

      List<SysUser> queryResult = USERS.stream()
            .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
            .collect(Collectors.toList());
      return CommonResult.success(queryResult);
   }

   /**
    * 根據手機號和郵箱查詢使用者資訊
    * @param phone 手機號
    * @return 使用者list
    */
   @GetMapping("selectByPhone")
   public CommonResult queryByPhone(@NotEmpty(message = "手機號不能為空") String phone) {
      List<SysUser> queryResult = USERS.stream()
            .filter(obj -> obj.getPhone().equals(phone))
            .collect(Collectors.toList());
      return CommonResult.success(queryResult);
   }

   /**
    * 新增使用者資訊
    * @param sysUser 使用者資訊
    * @return 成功標識
    */
   @PostMapping
   public CommonResult add(@Validated(value = ValidationInterface.add.class) @RequestBody SysUser sysUser) {
      Long id = (long) (USERS.size() + 1);
      sysUser.setId(id);
      USERS.add(sysUser);
      return CommonResult.success("新增成功");
   }

   /**
    * 根據Id更新使用者資訊
    * @param sysUser 使用者資訊
    * @return 成功標識
    */
   @PutMapping("{id}")
   public CommonResult updateById(@PathVariable("id") Long id,
                                  @Validated(value = ValidationInterface.update.class)
                                  @RequestBody SysUser sysUser)
   {
      for(int i = 0; i < USERS.size(); i++) {
         if(USERS.get(i).getId().equals(id)) {
            USERS.set(i,sysUser);
         }
      }
      return CommonResult.success("更新成功");
   }

   /**
    * 根據Id刪除使用者資訊
    * @param id 主鍵
    * @return 成功標識
    */
   @DeleteMapping("{id}")
   public CommonResult deleteById(@PathVariable Long id) {
      USERS.removeIf(obj -> obj.getId().equals(id));
      return CommonResult.success("刪除成功");
   }

   /**
    * 測試業務異常
    */
   @GetMapping("testException")
   public CommonResult testException(String name) {
      if(!"張三".equals(name)){
         throw new BusinessException("只有張三才可以訪問");
      }
      return CommonResult.success();
   }
}

7.4 啟動測試

查詢:

​ 輸出錯誤的郵箱

根據手機號查詢:

​ 輸入空值手機號

新增:

​ 輸入錯誤的手機號

測試主動丟擲業務異常:

8. 自定義驗證資訊源

8.1 修改配置檔案

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import javax.validation.Validator;
import java.util.Properties;

/**
 * 配置 Hibernate 引數校驗
 * @author ludangxin
 * @date 2021/8/5
 */
@Configuration
public class ValidatorConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        postProcessor.setValidator(validator);
        return postProcessor;
    }

    /**
     * 實體類欄位校驗國際化引入
     */
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        // 設定messages資源資訊
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        // 多個用逗號分割
        messageSource.setBasenames("classpath:/messages/validation/messages");
        // 設定字符集編碼
        messageSource.setDefaultEncoding("UTF-8");
        validator.setValidationMessageSource(messageSource);
        // 設定驗證相關引數
        Properties properties = new Properties();
        // 快速失敗,只要有錯馬上返回
        properties.setProperty("hibernate.validator.fail_fast", "true");
        validator.setValidationProperties(properties);
        return validator;
    }
}

8.2 新增資訊原始檔

├───resources
    └── messages
        └── validation
            └── messages.properties
# messages.properties
name.not.empty=使用者名稱不能為空
email.not.valid=${validatedValue}是郵箱地址?
email.not.empty=郵箱不能為空
phone.not.valid=${validatedValue}是手機號?
phone.not.empty=手機號不能為空
password.size.valid=密碼長度必須在{min}-{max}之間
id.not.empty=主鍵不能為空

8.3 修改實體類

import com.ldx.valid.annotation.Phone;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 使用者資訊管理
 * @author ludangxin
 * @date 2021/8/5
 */
@Data
public class SysUser implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主鍵
     */
    @NotNull(message = "{id.not.empty}", groups = {ValidationInterface.update.class})
    private Long id;

    /**
     * 使用者名稱
     */
    @NotEmpty(message = "{name.not.empty}", groups = {
              ValidationInterface.update.class,
              ValidationInterface.add.class})
    private String username;

    /**
     * 密碼
     */
    @Size(min = 6, max = 16, message = "{password.size.valid}", groups = {
          ValidationInterface.update.class,
          ValidationInterface.add.class})
    private String password = "123456";

    /**
     * 郵箱地址
     */
    @Email(message = "{email.not.valid}",
           groups = {
           ValidationInterface.update.class,
           ValidationInterface.add.class,
           ValidationInterface.select.class})
    @NotEmpty(message = "{email.not.empty}", groups = ValidationInterface.add.class)
    private String email;

    /**
     * 電話
     */
    @Pattern(message = "{phone.not.valid}", regexp = "^1[3456789]\\d{9}$", 
             groups = {ValidationInterface.add.class})
    @NotEmpty(message = "{phone.not.empty}", groups = {ValidationInterface.add.class})
    private String phone;
}

8.4 啟動測試

​ 輸入錯誤的郵箱地址測試:

9. 預置註解清單

註解 說明
@Null 限制只能為null
@NotNull 限制必須不為null
@AssertFalse 限制必須為false
@AssertTrue 限制必須為true
@DecimalMax(value) 限制必須為一個不大於指定值的數字
@DecimalMin(value) 限制必須為一個不小於指定值的數字
@Digits(integer,fraction) 限制必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 限制必須是一個將來的日期
@Max(value) 限制必須為一個不大於指定值的數字
@Min(value) 限制必須為一個不小於指定值的數字
@Past 限制必須是一個過去的日期
@Pattern(value) 限制必須符合指定的正規表示式
@Size(max,min) 限制字元長度必須在min到max之間
@Past 驗證註解的元素值(日期型別)比當前時間早
@NotEmpty 驗證註解的元素值不為null且不為空(字串長度不為0、集合大小不為0)
@NotBlank 驗證註解的元素值不為空(不為null、去除首位空格後長度為0),不同於@NotEmpty,@NotBlank
@Email 驗證註解的元素值是Email,也可以通過正規表示式和flag指定自定義的email格式

相關文章