Validation不是只能用注解,還可以通過編程方式實(shí)現(xiàn)參數(shù)校驗(yàn)
在項(xiàng)目中集成Hibernate-Validation,定義注解,實(shí)現(xiàn)參數(shù)的校驗(yàn),相信大家都會。
但如果我們需要校驗(yàn)的類是第三方提供的,由于種種原因無法替換參數(shù)類。根據(jù)業(yè)務(wù)邏輯,我們又需要對參數(shù)執(zhí)行特定的校驗(yàn)規(guī)則,應(yīng)該怎么做呢?當(dāng)注解沒有辦法使用時,我們就可以使用編程式約束了。
接下來,我們一起看下如何實(shí)現(xiàn)。
一、用例描述
我們先引入一個User實(shí)體類,假設(shè)這個類是由第三方提供的:
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
}
我們需要驗(yàn)證User類的id和name字段,id必須是正數(shù)、name不能為空。
如果能夠修改User類,我們只需要@NotNull和@Range(min = 1)兩個注解就解決問題了?,F(xiàn)在,我們需要迂回一下。
為了驗(yàn)證User的id字段,我們創(chuàng)建了一個名為UserId的自定義注解:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.hibernate.validator.constraints.Range;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.NotNull;
@Documented
@NotNull
@Range(min = 1)
@ReportAsSingleViolation
@Constraint(validatedBy = {})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface UserId {
String message() default "${validatedValue} must be a positive long";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
有了注解,還需要有約束定義類:
import org.hibernate.validator.cfg.ConstraintDef;
public class UserIdDef extends ConstraintDef<UserIdDef, UserId> {
public UserIdDef() {
super(UserId.class);
}
}
這里說個題外話,有朋友留言說我不寫明引用的包,想想也是,Java棧同名類那么多,不寫包名,很容易引起歧義。
此外,User的name字段不能為空,我們直接復(fù)用NotNull注解和NotNullDef約束定義。
二、驗(yàn)證器設(shè)置
現(xiàn)在,讓我們來研究一下驗(yàn)證器配置:
import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.hibernate.validator.cfg.context.TypeConstraintMappingContext;
import org.hibernate.validator.cfg.defs.NotNullDef;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
import cn.howardliu.effective.spring.constraint.UserIdDef;
import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
@Configuration
class ValidationConf {
@Bean
Validator validator(AutowireCapableBeanFactory autowireCapableBeanFactory) {
final HibernateValidatorConfiguration conf = Validation.byProvider(HibernateValidator.class).configure();
final ConstraintMapping constraintMapping = conf.createConstraintMapping();
final TypeConstraintMappingContext<User> context = constraintMapping.type(User.class);
context.field("id").constraint(new UserIdDef());
final NotNullDef notNullDef = new NotNullDef();
notNullDef.message("must not be null");
context.field("name").constraint(notNullDef);
return conf.allowOverridingMethodAlterParameterConstraint(true)
.addMapping(constraintMapping)
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
.buildValidatorFactory()
.getValidator();
}
}
我們使用TypeConstraintMappingContext,將必要的注解分配給User的id和name字段,以實(shí)現(xiàn)對應(yīng)的約束定義。
為了保障測試用例的準(zhǔn)確,這里有個小細(xì)節(jié):
final NotNullDef notNullDef = new NotNullDef();
notNullDef.message("must not be null");
我們在定義NotNullDef時,設(shè)置了message屬性。這是因?yàn)樵诋?dāng)前的hibernate-validation版本中,內(nèi)置了很多的錯誤信息,存儲在 ValidationMessages*.properties文件族中,不同語言的錯誤信息不同。
為了實(shí)現(xiàn)測試用例的穩(wěn)定性,統(tǒng)一設(shè)置為“must not be null”。
三、應(yīng)用驗(yàn)證邏輯
接下來我們定義一個接收User參數(shù)的組件:
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.Valid;
@Validated
@Service
public class UserService {
public void handleUser(@Valid User user) {
System.out.println("Got validated user " + user);
}
}
此時,如果向UserService的handleUser傳入?yún)?shù),就會執(zhí)行校驗(yàn)邏輯。
四、測試約束
編寫測試用例,看看執(zhí)行效果:
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import cn.howardliu.effective.spring.entity.User;
import jakarta.validation.ConstraintViolationException;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void provideInvalidUser() {
final User user = new User();
user.setId(-1L);
user.setName(null);
Assertions.assertThatThrownBy(() -> userService.handleUser(user))
.isInstanceOf(ConstraintViolationException.class)
.hasMessageContaining("handleUser.arg0.id: -1 must be a positive long")
.hasMessageContaining("handleUser.arg0.name: must not be null");
}
@Test
void provideValidUser() {
final User user = new User();
user.setId(1L);
user.setName("howardliu.cn");
assertDoesNotThrow(() -> userService.handleUser(user));
}
}
我們寫了兩個測試用例,一個是非法的參數(shù)、一個是合法參數(shù)。非法參數(shù)傳入時,會被攔截,并返回定義好的異常信息。
五、總結(jié)
本文中,我們討論了以編程方式為實(shí)體類添加驗(yàn)證的方式??梢孕薷脑创a時,我們可以使用注解,不能修改時,我們使用編程式校驗(yàn),幾乎可以覆蓋絕大部分場景了。