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,也可以通過正規表示式和flag指定自定義的email格式 |