在项目中集成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));
}
}
我们写了两个测试用例,一个是非法的参数、一个是合法参数。非法参数传入时,会被拦截,并返回定义好的异常信息。
五、总结
本文中,我们讨论了以编程方式为实体类添加验证的方式。可以修改源码时,我们可以使用注解,不能修改时,我们使用编程式校验,几乎可以覆盖绝大部分场景了。