Spring Boot之Validation自定義實現方式的總結

Validation自定義實現方式

Spring Boot Validation定制

雖然在Spring Boot中已經提供瞭非常多的預置註解,用以解決在日常開發工作中的各類內容,但是在特定情況仍然存在某些場景,無法滿足需求,需要自行定義相關的validator。本節將針對自定義的validator進行介紹。

自定義的註解

這裡的場景設置為進行IP地址的驗證,通過註解的方式,讓用戶使用驗證規則。註解定義如下:

@Target({ElementType.FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = IPAddressValidator.class)
public @interface IPAddress {
    String message() default "{ipaddress.invalid}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

這個註解是作用在Field字段上,運行時生效,觸發的是IPAddressValidator這個驗證類。

  • message
  • 定制化的提示信息,主要是從ValidationMessages.properties裡提取,也可以依據實際情況進行定制
  • groups
  • 這裡主要進行將validator進行分類,不同的類group中會執行不同的validator操作
  • payload
  • 主要是針對bean的,使用不多。

然後自定義Validator,這個是真正進行驗證的邏輯代碼:

public class IPAddressValidator implements ConstraintValidator<IPAddress, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Pattern pattern = compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
        Matcher matcher = pattern.matcher(value);
        try {
            if (!matcher.matches()) {
                return false;
            } else {
                for (int i = 1; i <= 4; i++) {
                    int octet = Integer.valueOf(matcher.group(i));
                    if (octet > 255) {
                        return false;
                    }
                }
                return true;
            }
        } catch (Exception e) {
            return false;
        }
    }
}

關於IP地址的驗證規則是通用的,具體邏輯不用太在意,主要是需要這裡Validator這個接口,以及其中的兩個泛型參數,第一個為註解名稱,第二個為實際字段的數據類型。

使用自定義的註解

定義瞭實體類CustomFieldBean.java

@Data
public class CustomFieldBean {
    @IPAddress
    private String ipAddr;
}

使用方法非常簡約,基於註解,無侵入邏輯。

單元測試用例

測試代碼:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomFieldValidatorTest {
    @Autowired
    private ProductService productService;
    @Test(expected = ConstraintViolationException.class)
    public void testInvalid() {
        CustomFieldBean customFieldBean = new CustomFieldBean();
        customFieldBean.setIpAddr("1.2.33");
        this.productService.doCustomField(customFieldBean);
    }
    @Test
    public void testValid() {
        CustomFieldBean customFieldBean = new CustomFieldBean();
        customFieldBean.setIpAddr("1.2.33.123");
        this.productService.doCustomField(customFieldBean);
    }
}

自定義執行Validator

如果不希望由系統自行觸發Validator的驗證邏輯,則可以由開發者自行進行驗證。在Spring Boot已經內置瞭Validator實例,直接將其加載進來即可。

使用示例如下:

@Autowired
private Validator validator;

自定義執行的單元測試

測試代碼如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CodeValidationTest {
    @Autowired
    private Validator validator;
    @Test(expected = ConstraintViolationException.class)
    public void testValidator() {
        CustomFieldBean input = new CustomFieldBean();
        input.setIpAddr("123.3.1");
        Set<ConstraintViolation<CustomFieldBean>> violations = validator.validate(input);
        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(violations);
        }
    }
}

自定義Validation註解

最近新開瞭一個項目,雖然hibernate-validator很好用,但是有時不能滿足稍微復雜一些的業務校驗。為瞭不在業務代碼中寫校驗邏輯,以及讓代碼更優雅,故而采用瞭自定義校驗註解的方式。

場景說明

本例註解應用場景: 填寫表單時,某一項數據存在時,對應的一類數據都應存在,一同提交。

源碼

1.類註解

主註解用於標記要在校驗的實體類

@Target( { TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = RelateOtherValidator.class)
@Documented
public @interface RelateOther {
    String message() default "";
    /**
     * 校驗數量
     */
    int num() default 2;
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

2.輔助註解

輔助註解用於標註於要校驗的字段,isMaster區分為主註解和從註解。

主註解是關鍵字段,存在才進行校驗從註解對應字段的有效性;主註解的value()屬性可以設置默認值,當字段對應值對應value()時才開啟校驗。

從註解為等待校驗的值,默認為從註解。

@Target( { FIELD })
@Retention(RUNTIME)
@Documented
public @interface RelateOtherItem {
    /**
     * 是否為主字段,主字段存在才進行校驗
     */
    boolean isMaster() default false;
    /**
     * 用於開啟對指定值校驗判斷,master字段有效
     * 當前為master且value與標註字段值相等才進行校驗,
     */
    String value() default "";
}

3.校驗類

校驗類為實際執行校驗邏輯的類,在類註解的@Constraint的validatedBy屬性上設置。

要設置為校驗類,首先要實現ConstraintValidator類的isValid方法。

@Slf4j  // @Slf4j是lombok的註解
public class RelateOtherValidator implements ConstraintValidator<RelateOther, Object> {
    // 要校驗的個數
    private int validateNum;
    @Override
    public void initialize(RelateOther constraintAnnotation) {
        validateNum = constraintAnnotation.num();
    }
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        if (o == null) {
            return true;
        }
        Field[] declaredFields = o.getClass().getDeclaredFields();
        boolean mater = false;
        int emptyNum = 0;
        try {
            // 總共需要校驗的字段數
            int totalValidateNum = validateNum;
            for (Field field : declaredFields) {
                // 校驗是否進行過標註
                if (!field.isAnnotationPresent(RelateOtherItem.class)) {
                    continue;
                }
                if (validateNum > 0 && totalValidateNum-- < 0) {
                    return false;
                }
                field.setAccessible(true);
                Object property = field.get(o);
                RelateOtherItem relateOtherItem = field.getAnnotation(RelateOtherItem.class);
                // 主字段不存在,則校驗通過
                if (relateOtherItem.isMaster()) {
                    if (property==null) {
                        return true;
                    }
                    // 與指定值不一致,校驗通過
                    if (!StringUtils.isEmpty(relateOtherItem.value()) && !relateOtherItem.value().equals(property)) {
                        return true;
                    }
                    mater = true;
                    continue;
                }
                if (null == property) {
                    emptyNum++;
                }
            }
            // 主字段不存在,則校驗通過
            if (!mater) {
                log.info("RelateOther註解主字段不存在");
                return true;
            }
            return emptyNum==0;
        } catch (Exception e) {
            log.info("RelateOther註解,解析異常 {}", e.getMessage());
            return false;
        }
    }
}

4.校驗失敗

註解校驗不同時會拋出一個MethodArgumentNotValidException異常。這裡可以采用全局異常處理的方法,進行捕獲處理。捕獲之後的異常可以獲取BindingResult 對象,後面就跟hibernate-validator處理方式一致瞭。

BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();

5.使用demo

註解的使用類似下面,首先在請求實體類上標註類註解,再在對應的字段上標註輔助註解。

@RelateOther(message = "xx必須存在!",num=2)
public class MarkReq  {
    @RelateOtherItem (isMaster= true,value="1")
    private Integer  girl;
    @RelateOtherItem 
    private Integer sunscreen;
    private String remarks;
}

總結

自定義註解在開發中還是很好用的,本文主要起到拋磚引玉的作用。對於首次使用的朋友應該還是有些用處的。

這些僅為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: