Validation不是只能用注解,還可以通過編程方式實現參數校驗
在項目中集成Hibernate-Validation,定義注解,實現參數的校驗,相信大家都會。
但如果我們需要校驗的類是第三方提供的,由于種種原因無法替換參數類。根據業務邏輯,我們又需要對參數執行特定的校驗規則,應該怎么做呢?當注解沒有辦法使用時,我們就可以使用編程式約束了。
接下來,我們一起看下如何實現。
一、用例描述
我們先引入一個User實體類,假設這個類是由第三方提供的:
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
}
我們需要驗證User類的id和name字段,id必須是正數、name不能為空。
如果能夠修改User類,我們只需要@NotNull和@Range(min = 1)兩個注解就解決問題了。現在,我們需要迂回一下。
為了驗證User的id字段,我們創建了一個名為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字段不能為空,我們直接復用NotNull注解和NotNullDef約束定義。
二、驗證器設置
現在,讓我們來研究一下驗證器配置:
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字段,以實現對應的約束定義。
為了保障測試用例的準確,這里有個小細節:
final NotNullDef notNullDef = new NotNullDef();
notNullDef.message("must not be null");
我們在定義NotNullDef時,設置了message屬性。這是因為在當前的hibernate-validation版本中,內置了很多的錯誤信息,存儲在 ValidationMessages*.properties文件族中,不同語言的錯誤信息不同。
為了實現測試用例的穩定性,統一設置為“must not be null”。
三、應用驗證邏輯
接下來我們定義一個接收User參數的組件:
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傳入參數,就會執行校驗邏輯。
四、測試約束
編寫測試用例,看看執行效果:
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));
}
}
我們寫了兩個測試用例,一個是非法的參數、一個是合法參數。非法參數傳入時,會被攔截,并返回定義好的異常信息。
五、總結
本文中,我們討論了以編程方式為實體類添加驗證的方式。可以修改源碼時,我們可以使用注解,不能修改時,我們使用編程式校驗,幾乎可以覆蓋絕大部分場景了。