我們在項目開發中,經常會對一些參數進行校驗,比如非空校驗、長度校驗,以及定製的業務校驗規則等,如果使用if/else語句來對請求的每一個參數一一校驗,就會出現大量與業務邏輯無關的代碼,繁重不堪且繁瑣的校驗,會大大降低我們的工作效率,而且準確性也無法保證。為保證數據的正確性、完整性,前後端都需要進行數 ...
我們在項目開發中,經常會對一些參數進行校驗,比如非空校驗、長度校驗,以及定製的業務校驗規則等,如果使用if/else語句來對請求的每一個參數一一校驗,就會出現大量與業務邏輯無關的代碼,繁重不堪且繁瑣的校驗,會大大降低我們的工作效率,而且準確性也無法保證。為保證數據的正確性、完整性,前後端都需要進行數據檢驗。本文對開源 boot-admin 項目的後端校驗實踐進行總結,以饗碼友。
boot-admin 是一款採用前後端分離模式、基於 SpringCloud 微服務架構的SaaS後臺管理框架。系統內置基礎管理、許可權管理、運行管理、定義管理、代碼生成器和辦公管理6個功能模塊,集成分散式事務 Seata、工作流引擎 Flowable、業務規則引擎 Drools、後臺作業調度框架 Quartz 等,技術棧包括 Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。
引入Maven依賴
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
參數校驗實踐
定義校驗對象
@Data
/** 組合校驗註解(方式1) **/
@OverallValid(value = "check1" ,message="女士不得小於16歲。")
@OverallValid(value = "check2" ,message="男士不得小於18歲。")
public class User {
//字元個數檢測(內置註解)
@Size(min = 1,max = 10,message = "姓名長度必須為1到10")
//占用空間長度檢測(自定義註解)
@StringLength(min = 1,max = 12,message = "姓名的保存長度不允許超過12個位元組。")
private String name;
//利用枚舉類檢測(自定義註解)
@EnumValid(target = SexEnum.class, message = "性別的取值範圍是【1】和【2】")
private String sex;
//註意 @NotNull @NotEmpty @NotBlank 的區別
@NotBlank(message = "姓氏是必填項。")
private String firstName;
@Min(value = 10,message = "年齡最小為10")
@Max(value = 100,message = "年齡最大為100")
private Integer age;
@Past(message = "出生時間必須為過去時間")
private Date birth;
@NotEmpty(message = "興趣不能為空")
private List<String> interest;
//嵌套檢測
@Valid
private List<User> children;
@Valid
private User father;
@Valid
private User mother;
/** 組合校驗(方式2) **/
@BooleanValid(message = "男性年齡需在60歲以下")
public boolean getValid1(){
if(sex.equalsIgnoreCase("1") && age >= 60 ){
return false;
}
return true;
}
/** 組合校驗(方式2) **/
@BooleanValid(message = "女性年齡需在55歲以下")
public boolean getValid2(){
if(sex.equalsIgnoreCase("2") && age >= 55 ){
return false;
}
return true;
}
/** 組合校驗(方式1)方法 **/
public boolean check1(){
if(sex.equalsIgnoreCase("2") && age < 16 ){
return false;
}
return true;
}
/** 組合校驗(方式1)方法 **/
public boolean check2(){
if(sex.equalsIgnoreCase("1") && age < 18 ){
return false;
}
return true;
}
}
相關枚舉類:
public enum SexEnum {
男("1"),女("2");
private final String value;
SexEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
參數校驗(在 Controller 中使用)
@RestController
@RequestMapping("/api/system")
@Slf4j
public class DemoController {
//註入校驗信息採集器
@Resource
private FormValidator formValidator;
@PostMapping("/free/user/check")
public ResultDTO check(@Valid @RequestBody User user, BindingResult bindingResult, HttpServletRequest request) throws Exception{
/** 參數校驗 **/
if (bindingResult.hasErrors()) {
return formValidator.generateMessage(bindingResult);
}
/** 繼續執行業務邏輯 **/
return ResultDTO.success();
}
}
在Controller中使用的校驗結果信息採集器實現
介面定義:
public interface FormValidator {
ResultDTO generateMessage(BindingResult bindingResult) throws Exception;
}
類實現:
@Service
@Slf4j
public class FormValidatorImpl implements FormValidator {
@Override
public ResultDTO generateMessage(BindingResult bindingResult) throws Exception {
String msg = this.getMessage(bindingResult);
return ResultDTO.failureCustom(msg);
}
/**
* 生成校驗結果
* @param bindingResult
* @return
*/
private String getMessage(BindingResult bindingResult){
log.info(bindingResult.toString());
List<ObjectError> objectErrorList=bindingResult.getAllErrors();
String msg= this.getFormValidErrsMsgNoBr(objectErrorList);
log.info(msg);
return msg;
}
private String getFormValidErrsMsgNoBr(List<ObjectError> objectErrorList) {
if (objectErrorList==null) {
return "";
}
StringBuffer csv = new StringBuffer();
csv.append("數據驗證未通過:[");
for (int i = 0; i < objectErrorList.size(); i++){
if (i > 0){
csv.append("],[");
}
csv.append(objectErrorList.get(i).getDefaultMessage());
}
csv.append("]");
return csv.toString();
}
}
相關註解介紹
JSR-303 規範常用註解
以下列舉常用內置註解,可直接使用。
註解 | 描述 |
---|---|
@Valid | 對po實體盡心校驗 |
@AssertFalse | 所註解的元素必須是Boolean類型,且值為false |
@AssertTrue | 所註解的元素必須是Boolean類型,且值為true |
@DecimalMax | 所註解的元素必須是數字,且值小於等於給定的值 |
@DecimalMin | 所註解的元素必須是數字,且值大於等於給定的值 |
@Digits | 所註解的元素必須是數字,且值必須是指定的位數 |
@Future | 所註解的元素必須是將來某個日期 |
@Max | 所註解的元素必須是數字,且值小於等於給定的值 |
@Min | 所註解的元素必須是數字,且值大於等於給定的值 |
@Range | 所註解的元素需在指定範圍區間內 |
@NotNull | 所註解的元素值不能為null |
@NotBlank | 所註解的元素值有內容 |
@Null | 所註解的元素值為null |
@Past | 所註解的元素必須是某個過去的日期 |
@PastOrPresent | 所註解的元素必須是過去某個或現在日期 |
@Pattern | 所註解的元素必須滿足給定的正則表達式 |
@Size | 所註解的元素必須是String、集合或數組,且長度大小需保證在給定範圍之內 |
所註解的元素需滿足Email格式 |
自定義註解
僅僅使用內置的註解,無法滿足複雜的業務需求,故擴展下麵幾個自定義註解。
UTF-8 字元串長度校驗
對字元串長度的校驗目的,一般是用於保證數據表欄位可以容納,當字元串內容是中文時,內置的 @Size 是不適用的,此時就需要自行擴展 UTF-8 字元串長度校驗。
註解類:
@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {StringLengthValidator.class})
public @interface StringLength {
int max() default 4000;
int min() default 0;
String message() default "字元串長度不符合要求。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
註解類實現:
@Slf4j
public class StringLengthValidator implements ConstraintValidator<StringLength, String> {
private int max;
private int min;
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
try {
if(StringUtils.isBlank(value)){
if(min > 0){
return false;
}else {
return true;
}
}
byte[] tmpbyte = value.getBytes("UTF-8");
int length = tmpbyte.length;
if(length < min || length > max){
return false;
}
return true;
}catch (Exception ex){
log.error("註解校驗StringLength發生異常。");
log.error(ex.getMessage(),ex);
return false;
}
}
@Override
public void initialize(StringLength constraintAnnotation) {
max = constraintAnnotation.max();
min = constraintAnnotation.min();
}
}
手機號碼校驗
註解類:
@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {
String regexp() default "";
String message() default "手機號碼格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
註解類實現:
public class MobileValidator implements ConstraintValidator<Mobile, String> {
/**
* 手機號的正則表達式.
*/
private static Pattern pattern = Pattern.compile(
"^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
Matcher m = pattern.matcher(value);
return m.matches();
}
@Override
public void initialize(Mobile constraintAnnotation) {}
}
這裡對手機號碼的校驗使用了正則表達式,也可以直接使用內置註解 @Pattern 定義校驗規則。
枚舉類整數值校驗
有時需要校驗參數值必須是系統定義的枚舉值(整數值),此時需要擴展以下註解。
註解類:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumIntegerValidator.class})
public @interface EnumIntegerValid {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/** * 目標枚舉類 */
Class<?> target() default Class.class;
/** * 是否忽略空值 */
boolean ignoreEmpty() default true;
}
註解類實現:
@Slf4j
public class EnumIntegerValidator implements ConstraintValidator<EnumIntegerValid, Integer> {
/** 枚舉校驗註解 */
private EnumIntegerValid annotation;
@Override
public void initialize(EnumIntegerValid constraintAnnotation) {
annotation = constraintAnnotation;
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean result = false;
Class<?> cls = annotation.target();
boolean ignoreEmpty = annotation.ignoreEmpty();
// target為枚舉,並且value有值,或者不忽視空值,才進行校驗
if (cls.isEnum() && value != null) {
Object[] objects = cls.getEnumConstants();
try {
Method method = cls.getMethod("getValue");
for (Object obj : objects) {
Object code = method.invoke(obj);
if (value.compareTo((Integer) code) == 0) {
result = true;
break;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.warn("EnumValidator call isValid() method exception.");
result = false;
}
} else {
result = true;
}
return result;
}
}
枚舉類字元串校驗
有時需要校驗參數值必須是系統定義的枚舉值(字元串),此時需要擴展以下註解。
註解類:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValidator.class})
public @interface EnumValid {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/** * 目標枚舉類 */
Class<?> target() default Class.class;
/** * 是否忽略空值 */
boolean ignoreEmpty() default true;
}
註解類實現:
@Slf4j
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
/** 枚舉校驗註解 */
private EnumValid annotation;
@Override
public void initialize(EnumValid constraintAnnotation) {
annotation = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
boolean result = false;
Class<?> cls = annotation.target();
boolean ignoreEmpty = annotation.ignoreEmpty();
// target為枚舉,並且value有值,或者不忽視空值,才進行校驗
boolean fitCheck = cls.isEnum() && (isNotEmpty(value) || !ignoreEmpty);
if (fitCheck) {
Object[] objects = cls.getEnumConstants();
try {
Method method = cls.getMethod("getValue");
for (Object obj : objects) {
Object code = method.invoke(obj);
if (value.equals(code.toString())) {
result = true;
break;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.warn("EnumValidator call isValid() method exception.");
result = false;
}
} else {
result = true;
}
return result;
}
}
Bean 內多屬性組合校驗(組合校驗)
此類校驗一般屬於業務邏輯校驗,常常要求多個屬性符合一定的邏輯設定。此時需要在Bean中編寫校驗方法,併在類定義前面添加自定義註解 @OverallValid 或者在方法前面加上自定義註解 @BooleanValid
方式1:
註解在類定義前面,類方法要求:
- 方法的可訪問屬性:public
- 方法的返回類型: boolean
@OverallValid註解類:
@Target({METHOD, FIELD,TYPE})
@Retention(RUNTIME)
@Repeatable(OverallValids.class)
@Documented
@Constraint(validatedBy = {OverallValidImpl.class})
public @interface OverallValid {
String value() default "overallValid";
String message() default "組合校驗未通過。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
上面註解要求可重覆使用,使用了 @Repeatable(OverallValids.class),OverallValids 代碼如下:
@Target({METHOD, FIELD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OverallValids {
OverallValid[] value();
}
使用註入的方法名,通過反射執行該方法,得到校驗結果。註解實現如下:
@Slf4j
public class OverallValidImpl implements ConstraintValidator<OverallValid, Object> {
private String functionName;
@Override
public void initialize(OverallValid overallValid) {
functionName = overallValid.value();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
try {
//得到方法對象
Method checkMethod = o.getClass().getMethod(functionName);
//調用方法,得到返回值
Object checkRet = checkMethod.invoke(o);
return Boolean.valueOf(checkRet.toString());
}catch (Exception ex){
log.error("綜合校驗異常。");
log.error(ex.getMessage(),ex);
}
return false;
}
}
方式2:
註解在方法前面,類方法要求:
- 方法的可訪問屬性:public
- 方法的返回類型: boolean
- 方法名稱格式:get+首字母大寫駝峰,如 getValid1
@BooleanValid註解類:
@Target( {
METHOD,
FIELD,
ANNOTATION_TYPE,
CONSTRUCTOR,
PARAMETER
})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {BooleanValidImpl.class})
public @interface BooleanValid {
boolean value() default true;
String message() default "綜合校驗未通過。";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
類實現:
@Slf4j
public class BooleanValidImpl implements ConstraintValidator<BooleanValid, Boolean> {
@Override
public boolean isValid(Boolean value, ConstraintValidatorContext constraintValidatorContext) {
return value;
}
@Override
public void initialize(BooleanValid constraintAnnotation) {
}
}
嵌套校驗
在成員屬性上加註解 @Valid ,意味著對該成員屬性進行嵌套校驗,校驗規則按該成員的內部校驗註解執行。
本文來自博客園,作者:超然樓,轉載請註明原文鏈接:https://www.cnblogs.com/soft1314/p/17380059.html