1. 簡介
我們都知道前臺的驗證只是為了滿足界面的友好性、客戶體驗性等等。但是如果僅靠前端進行數(shù)據(jù)合法性校驗,是遠遠不夠的。因為非法用戶可能會直接從客戶端獲取到請求地址進行非法請求,所以后臺的校驗是必須的;特別是應用如果不允許輸入空值,對數(shù)據(jù)的合法性有要求的情況下。
2. 開擼
2.1 項目結(jié)構
結(jié)構說明:
├── JAVA
│ └── com
│ └── ldx
│ └── valid
│ ├── ValidApplication.java # 啟動類
│ ├── annotation
│ │ └── Phone.java # 自定義驗證注解
│ ├── config
│ │ └── ValidatorConfig.java # 表單驗證配置類
│ ├── controller
│ │ └── SysUserController.java # 用戶管理控制器
│ ├── exception
│ │ ├── BusinessException.java # 業(yè)務異常類
│ │ └── GlobalExceptionHandler.java # 統(tǒng)一異常處理類
│ ├── model
│ │ ├── SysUser.java # 用戶信息實體
│ │ └── ValidationInterface.java # 表單驗證的通用分組接口
│ ├── util
│ │ └── CommonResult.java # 接口返回封裝類
│ └── validation
│ └── PhoneValidator.java #自定義驗證實現(xiàn)類
└── resources
├── application.yaml # 配置文件
└── messages
└── validation
└── messages.properties # 自定義驗證信息源
2.1 quick start
2.1.1 導入依賴
創(chuàng)建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 添加配置類
創(chuàng)建表單驗證配置類,配置快速校驗,不用等全部的參數(shù)校驗完,只要有錯,馬上拋出。
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 參數(shù)校驗
* @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 {
/** 狀態(tài)碼 */
private int code;
/** 返回內(nèi)容 */
private String msg;
/** 數(shù)據(jù)對象 */
private Object data;
/**
* 初始化一個新創(chuàng)建的 CommonResult 對象
* @param type 狀態(tài)類型
* @param msg 返回內(nèi)容
*/
public CommonResult(Type type, String msg) {
this.code = type.value;
this.msg = msg;
}
/**
* 初始化一個新創(chuàng)建的 CommonResult 對象
* @param type 狀態(tài)類型
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對象
*/
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("操作成功");
}
/**
* 返回成功數(shù)據(jù)
* @return 成功消息
*/
public static CommonResult success(Object data) {
return CommonResult.success("操作成功", data);
}
/**
* 返回成功消息
* @param msg 返回內(nèi)容
* @return 成功消息
*/
public static CommonResult success(String msg) {
return CommonResult.success(msg, null);
}
/**
* 返回成功消息
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對象
* @return 成功消息
*/
public static CommonResult success(String msg, Object data) {
return new CommonResult(Type.SUCCESS, msg, data);
}
/**
* 返回警告消息
* @param msg 返回內(nèi)容
* @return 警告消息
*/
public static CommonResult warn(String msg) {
return CommonResult.warn(msg, null);
}
/**
* 返回警告消息
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對象
* @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 返回內(nèi)容
* @return 錯誤消息
*/
public static CommonResult error(String msg) {
return CommonResult.error(msg, null);
}
/**
* 返回錯誤消息
* @param msg 返回內(nèi)容
* @param data 數(shù)據(jù)對象
* @return 錯誤消息
*/
public static CommonResult error(String msg, Object data) {
return new CommonResult(Type.ERROR, msg, data);
}
/**
* 狀態(tài)類型
*/
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<>();
// 數(shù)據(jù)初始化
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信息填錯,測試結(jié)果符合預期。
log日志:
[nio-8080-exec-9] c.l.valid.controller.SysUserController : [郵箱地址不合法]:字段==email 值==123
3. 分組校驗
groups是用來干什么的?
因為一個實體不可能只干一種操作,一個實體必然存在增刪改查操作,那么問題就來了
如果我要根據(jù)id進行更新操作,那么id肯定不能為空
這時候我還要進行新增操作,因為id是新增數(shù)據(jù)庫操作才產(chǎn)生的,接受數(shù)據(jù)的時候我肯定是沒有id的
所以就產(chǎn)生矛盾了
那么groups這個參數(shù)就起作用了,它可以表示我這個注解屬于哪個組,這樣就解決這個尷尬的問題了。
當在controller中校驗表單數(shù)據(jù)時,如果使用了groups,那么沒有在這個分組下的屬性是不會校驗的
3.1 添加分組接口
/**
* 用于表單驗證的通用分組接口
* @author ludangxin
* @date 2021/8/5
*/
public interface ValidationInterface {
/**
* 新增分組
*/
interface add{}
/**
* 刪除分組
*/
interface delete{}
/**
* 查詢分組
*/
interface select{}
/**
* 更新分組
*/
interface update{}
}
如果還有其它特殊的分組要求 直接在DO中創(chuàng)建interface即可
例:如果還有個需要驗證username 和 password(只有這兩個參數(shù)) 的 select操作
直接在SysUser中創(chuàng)建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<>();
// 數(shù)據(jù)初始化
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);
}
/**
* 根據(jù)手機號或郵箱查詢用戶信息
* @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("新增成功");
}
/**
* 根據(jù)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("更新成功");
}
/**
* 根據(jù)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. 自定義驗證
很多時候框架提供的功能并不能滿足我們的業(yè)務場景,這時我們需要自定義一些驗證規(guī)則來完成驗證。
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 {};
//載荷 將某些元數(shù)據(jù)信息與給定的注解聲明相關聯(lián)的方法
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. 調(diào)用過程驗證
有的時候我們在參數(shù)傳輸過程中需要對傳入的對象做參數(shù)驗證,但是上面介紹的都是對參數(shù)綁定時的驗證,那能不能使用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 啟動驗證
調(diào)用新增方法,通過新增方法調(diào)用validateParams方法
報錯日志如下
java.lang.RuntimeException: 手機號不合法:對象:SysUser,字段:phone,值:135999
5.2 非spring環(huán)境驗證
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. 方法參數(shù)驗證
有的時候我們想在方法上直接進行參數(shù)驗證,步驟如下
6.1 修改控制器
直接在類上添加注解@Validated,并在方法上直接進行驗證
@Slf4j
@Validated
@RestController
@RequestMapping("sys/user")
public class SysUserController {
... 省略代碼
/**
* 根據(jù)手機號和郵箱查詢用戶信息
* @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字段賦值,操作結(jié)果符合預期
錯誤日志:
javax.validation.ConstraintViolationException: queryByPhone.phone: 手機號不能為空
7. 統(tǒng)一異常處理
在上面的參數(shù)驗證中,驗證的錯誤信息是通過BindingResult result參數(shù)進行接收的,在每個方法中異常處理如出一轍,特別麻煩。甚至在step 5,6都是直接將異常的堆棧信息返回給前端,這對于用來說是非常不友好的。而且有的情況下需要我們主動拋出業(yè)務異常,比方用戶不能直接刪除已綁定用戶的角色。
所以,開擼。
7.1 創(chuàng)建業(yè)務異常類
/**
* 業(yè)務異常
* @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 創(chuàng)建全局異常處理器
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 {
/**
* 參數(shù)綁定異常類 用于表單驗證時拋出的異常處理
*/
@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);
}
/**
* 用于方法形參中參數(shù)校驗時拋出的異常處理
* @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());
}
/**
* 系統(tǒng)異常
*/
@ExceptionHandler(Exception.class)
public CommonResult handleException(Exception e) {
log.error(e.getMessage(), e);
return CommonResult.error("服務器錯誤,請聯(lián)系管理員");
}
/**
* 業(yè)務異常
*/
@ExceptionHandler(BusinessException.class)
public CommonResult businessException(HttpServletRequest request, BusinessException e) {
log.error(e.getMessage());
return CommonResult.error(e.getMessage());
}
}
7.3 修改控制器
刪除方法中的BindingResult result參數(shù),將錯誤直接拋給統(tǒng)一異常處理類去解決即可。
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<>();
// 數(shù)據(jù)初始化
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);
}
/**
* 根據(jù)手機號或郵箱查詢用戶信息
* @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);
}
/**
* 根據(jù)手機號和郵箱查詢用戶信息
* @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("新增成功");
}
/**
* 根據(jù)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("更新成功");
}
/**
* 根據(jù)Id刪除用戶信息
* @param id 主鍵
* @return 成功標識
*/
@DeleteMapping("{id}")
public CommonResult deleteById(@PathVariable Long id) {
USERS.removeIf(obj -> obj.getId().equals(id));
return CommonResult.success("刪除成功");
}
/**
* 測試業(yè)務異常
*/
@GetMapping("testException")
public CommonResult testException(String name) {
if(!"張三".equals(name)){
throw new BusinessException("只有張三才可以訪問");
}
return CommonResult.success();
}
}
7.4 啟動測試
查詢:
? 輸出錯誤的郵箱
根據(jù)手機號查詢:
? 輸入空值手機號
新增:
? 輸入錯誤的手機號
測試主動拋出業(yè)務異常:
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 參數(shù)校驗
* @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);
// 設置驗證相關參數(shù)
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) |
限制必須為一個不大于指定值的數(shù)字 |
@DecimalMin(value) |
限制必須為一個不小于指定值的數(shù)字 |
@Digits(integer,fraction) |
限制必須為一個小數(shù),且整數(shù)部分的位數(shù)不能超過integer,小數(shù)部分的位數(shù)不能超過fraction |
@Future |
限制必須是一個將來的日期 |
@Max(value) |
限制必須為一個不大于指定值的數(shù)字 |
@Min(value) |
限制必須為一個不小于指定值的數(shù)字 |
@Past |
限制必須是一個過去的日期 |
@Pattern(value) |
限制必須符合指定的正則表達式 |
@Size(max,min) |
限制字符長度必須在min到max之間 |
@Past |
驗證注解的元素值(日期類型)比當前時間早 |
@NotEmpty |
驗證注解的元素值不為null且不為空(字符串長度不為0、集合大小不為0) |
@NotBlank |
驗證注解的元素值不為空(不為null、去除首位空格后長度為0),不同于@NotEmpty,@NotBlank |
|
驗證注解的元素值是Email,也可以通過正則表達式和flag指定自定義的email格式 |
原文鏈接:
https://www.cnblogs.com/ludangxin/p/15113954.html作者:張鐵牛
如果覺得本文對你有幫助,可以轉(zhuǎn)發(fā)關注支持一下