在我們的日常編程中,我們會(huì)使用許多可用于驗(yàn)證的 Spring Boot 默認(rèn)注解,如@NotNull、@Size、@NotBlank、@Digits等等,這是驗(yàn)證任何傳入的一種很酷的方式要求。
考慮一個(gè)場景,默認(rèn)情況下有一些字段是可選的,如果其他一些字段由特定值填充,則它必須是強(qiáng)制性的。
Spring 沒有為這種驗(yàn)證預(yù)定義注釋。
讓我們舉一些例子,看看我們?nèi)绾魏喕?yàn)證過程,使其代碼可重用,并在注釋級別引入抽象。
在一個(gè)典型的銷售平臺(tái)中,會(huì)有銷售操作和無效銷售操作。該金額在銷售操作中是強(qiáng)制性的,在銷售操作無效的情況下,沖銷類型將是強(qiáng)制性的。
我們的 dto 類如下:
public class IncomingRequestDto {
public TransactionType transactionType;
public ReversalType reversalType;
public String reversalId;
public AmountDto amountDto;
}
IncomingRequestDto 有幾個(gè)屬性,如 transactionType、reversalType 作為 ENUMS。
public enum TransactionType {
SALE {
public String toString() {
return "Sale";
}
},
VOIDSALE {
public String toString() {
return "VoidSale";
}
},
}
public enum ReversalType {
TIMEDOUT {
public final String toString() {
return "Timedout";
}
},
CANCELLED {
public final String toString() {
return "Cancelled";
}
}
}
和 amountDto 為:
public class AmountDto {
public String value;
}
場景一: amountDto.value 是有條件的。當(dāng)我們收到一個(gè)具有 transactionType="SALE" 的請求時(shí),amountDto.value 應(yīng)該是強(qiáng)制性的。
場景 2: reversalType 是有條件的。當(dāng)我們收到一個(gè)具有 transactionType="VOIDSALE" 的請求時(shí),reversalType 應(yīng)該是強(qiáng)制性的。
讓我們首先定義一個(gè)帶有驗(yàn)證過程所需屬性的注釋:
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
public @interface NotNullIfAnotherFieldHasValue {
String fieldName();
String fieldValue();
String dependFieldName();
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
NotNullIfAnotherFieldHasValue[] value();
}
}
fieldName 和 fieldValue 將被定義,我們必須在其上搜索特定值。這里是“銷售”。
將定義dependFieldName,我們必須在其上搜索值。
現(xiàn)在讓我們實(shí)現(xiàn)上面的接口:
public class NotNullIfAnotherFieldHasValueValidator
implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
private String fieldName;
private String expectedFieldValue;
private String dependFieldName;
@Override
public void initialize(NotNullIfAnotherFieldHasValue annotation) {
fieldName = annotation.fieldName();
expectedFieldValue = annotation.fieldValue();
dependFieldName = annotation.dependFieldName();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext ctx) {
String fieldValue = "";
String dependFieldValue = "";
if (value == null) {
return true;
}
try {
fieldValue = BeanUtils.getProperty(value, fieldName);
dependFieldValue = BeanUtils.getProperty(value, dependFieldName);
return validate(fieldValue, dependFieldValue, ctx);
} catch (NestedNullException ex) {
dependFieldValue = StringUtils.isNotBlank(dependFieldValue) ? dependFieldValue : "";
try {
return validate(fieldValue, dependFieldValue, ctx);
} catch (NumberFormatException exception) {
return false;
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | NumberFormatException | NullPointerException ex) {
return false;
}
}
private boolean validate(String fieldValue,
String dependFieldValue, ConstraintValidatorContext ctx) {
if (!StringUtils.isBlank(fieldValue)) {
if (expectedFieldValue.equals(fieldValue) && (StringUtils.isBlank(dependFieldValue))) {
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
.addNode(dependFieldName)
.addConstraintViolation();
return false;
}
} else {
return false;
}
return true;
}
}
在這里,我們需要返回并使用其實(shí)現(xiàn)類裝飾我們的界面,如下所示:
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
@Documented
public @interface NotNullIfAnotherFieldHasValue {
就是這樣!我們已經(jīng)完成了!讓我們用我們的自定義注解裝飾我們的 IncomingRequestDto 類:
@JsonDeserialize(as = IncomingRequestDto.class)
@NotNullIfAnotherFieldHasValue.List({
@NotNullIfAnotherFieldHasValue(
fieldName = "transactionType",
fieldValue = "Sale",
dependFieldName = "amountDto.value",
message = " - amount is mandatory for Sale requests"),
})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class IncomingRequestDto {
public TransactionType transactionType;
public ReversalType reversalType;
public String reversalId;
public AmountDto amountDto;
}
通過添加上述注釋,請求將被拒絕為BAD,HTTP 400不為 Sale 類型請求填充 amountDto.value。我們可以在 List 中添加任意數(shù)量的驗(yàn)證,而無需更改任何代碼,如下所示:
@JsonDeserialize(as = IncomingRequestDto.class)
@NotNullIfAnotherFieldHasValue.List({
@NotNullIfAnotherFieldHasValue(
fieldName = "transactionType",
fieldValue = "Sale",
dependFieldName = "amountDto.value",
message = " - amount is mandatory for Sale requests"),
@NotNullIfAnotherFieldHasValue(
fieldName = "transactionType",
fieldValue = "VoidSale",
dependFieldName = "reversalType",
message = " - Reversal Type is mandatory for VoidSale requests"),
})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class IncomingRequestDto {
public TransactionType transactionType;
public ReversalType reversalType;
public String reversalId;
public AmountDto amountDto;
}
完整實(shí)現(xiàn)請參考 GitHub頁面。
同樣,在另一種情況下,有些字段默認(rèn)是可選的,如果其他兩個(gè)字段由特定值填充(兩個(gè)字段驗(yàn)證),則必須為必填字段。