首頁 > 軟體

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

2022-07-04 14:01:06

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;
}

總結

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

這些僅為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com