建议结合Hibernate-Validator和Jakarta Bean Validation规范学习。
如果忘记某个注解或功能如何使用了,可以快速参考Hibernate-Validator官方提供的示例
官方代码非常清晰,以每一章为一个目录
可以从jakarta-bean-validation-spec-3.0规范中找到全部注解以及约束条件
field-level
public class Car {
@NotNull
private String manufacturer;
@AssertTrue
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
//getters and setters...
}
property-level,这种方式和上面效果一样,必须是放到get方法上,不是set方法
The property’s getter method has to be annotated, not its setter. That way also read-only properties can be constrained which have no setter method
public class Car {
private String manufacturer;
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
@NotNull
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
@AssertTrue
public boolean isRegistered() {
return isRegistered;
}
public void setRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
}
constainer-level,包括List、Set、Map、Optional
Object graphs,一个JavaBean的属性是另外一个JavaBean,通过在需要校验的Bean上使用@Valid注解即可实现校验
public class Car {
private List<@NotNull @Valid Person> passengers = new ArrayList<Person>();
}
当然也可以这样写,不过官方不建议
public class Car {
@Valid private List<@NotNull Person> passengers = new ArrayList<Person>();
}
In versions prior to 6, Hibernate Validator supported cascaded validation for a subset of container elements and it was implemented at the container level (e.g. you would use @Valid private List to enable cascaded validation for Person).
This is still supported but is not recommended. Please use container element level @Valid annotations instead as it is more expressive.
class-level,即约束注解是放在类上的,而不是具体的属性上的
以上对JavaBean以及其属性的校验,是由Validator接口负责校验,它有如下几个方法
Validator#validate()
Validator#validateProperty()
Validator#validateValue()
具体使用可参考官方示例或者官方文档中关于Validator的文档中的示例代码
约束注解是可继承,当在父类或者接口中添加了约束注解,则子类及其实现会自动继承该约束注解
When a class implements an interface or extends another class, all constraint annotations declared on the super-type apply in the same manner as the constraints specified on the class itself
例如
public class RentalStation {
public RentalStation(@NotNull String name) {
}
public void rentCar(
@NotNull Customer customer,
@NotNull @Future Date startDate,
@Min(1) int durationInDays) {
}
}
public class RentalStation {
@ValidRentalStation
public RentalStation() {
}
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getCustomers() {
return null;
}
}
对普通方法、构造方法的入参和返回值的校验由ExecutableValidator负责。它有以下几个方法
validateParameters()
validateReturnValue()
validateConstructorParameters()
validateConstructorReturnValue()
具体使用可参考官方示例或者官方文档中关于ExecutableValidator的文档中的示例代码
public interface Vehicle {
void drive(@Max(75) int speedInMph);
}
public class Car implements Vehicle {
@Override
public void drive(@Max(55) int speedInMph) {
//...
}
}
public interface Vehicle {
void drive(@Max(75) int speedInMph);
}
public interface Car {
void drive(int speedInMph);
}
public class RacingCar implements Car, Vehicle {
@Override
public void drive(int speedInMph) {
//...
}
}
有关上述更详细的解释移步官方文档
每一个注解上都有一个message属性,当校验失败时,会抛出此信息。平时开发中如果我们不需要做国际化处理,那么直接在message属性后面写对应的error message即可。
但是如果我们的项目的工程师团队涉及到多个国家,那么当不同地区的工程师排查问题时,查看到的错误日志如果是国际化的,那么对于排查问题的工程师就非常友好了。
有过Web开发经验的Java工程师可能都了解ResourceBundle这个类,这个类就是负责国际化的,一个简单的例子
// language和country实际中可以在Accept-Lanuage请求头中获取
Locale locale = new Locale(language, country);
// 根据Local来获取不同的ResourceBundle
ResourceBundle resourceBundle = ResourceBundle.getBundle("message", locale, I18nUtil.getResourceBundleClassLoader());
// 消息的key
return resourceBundle.getString("hello");
就三行代码,我们在项目的resource目录下建立不同国家的message_languageCode_countryCode.properties文件,
Hibernate-Validator也是使用了相同的方法,不过i18n配置文件的前缀需要使用ValidationMessages,为什么呢?因为这是Bean Validation规范里定的。
如果你就不想使用ValidationMessages这个名字,那么你也可以自己实现MessageInterpolator。具体实现的细节可以参考Hibernate-Validator官方文档和Bean Validation规范文档。这一部分不在本篇文章里做过多讨论
key用{}括起来
@Getter
@Setter
public class Person {
@Size(min = 3, max = 5, message = "{userName.invalid}")
private String userName;
@Max(value = 10, message = "{userAge.invalid}")
private Integer userAge;
}
这里定义两个文件ValidationMessages_zh_CN.properties和ValidationMessages_en_US.properties来做测试。二者均放在resource目录下
userName.invalid= userName ${validatedValue} is invalid
userAge.invalid= max value is {value}, but current value is ${validatedValue}
userName.invalid=\u7528\u6237\u540d ${validatedValue} \u662f\u975e\u6cd5\u7684
userAge.invalid=\u6700\u5927\u503c\u662f {value}, \u4f46\u662f\u5f53\u524d\u503c\u662f ${validatedValue}
汉字部分需要转成Unicode在每个key后面的实际内容中,上述例子有两个特殊值,
Message parameters are string literals enclosed in {},
while message expressions are string literals enclosed in ${}
如果看一下Hibernate-Validator源码会发现,源码文件中也包含了很多ValidationMessages文件,那么我们自定义了之后,会不会覆盖了呢?
一般来说,Hibernate-Validator里的ValidationMessages文件内容都是约束注解的默认信息,对于我们自定义的的key是不会有冲突的。关于查找key的规则查看Hibernate-Validator关于Message interpolation的文档和Bean Validation关于Message interpolation的文档
本篇文章使用Restful Web Service的框架不是SpringMVC,而是RestEasy.
启动项目之后,curl访问
curl --header "Accept-Language: en-US" --json '{"userName":"tty", "userAge":40}' http://localhost:8080/validation-demo/valid/inline
curl --header "Accept-Language: zh-CN" --json '{"userName":"tty", "userAge":40}' http://localhost:8080/validation-demo/valid/inline
可以看到,返回的错误信息是根据我们的Accept-Language的请求头来国际化错误信息的
Hibernate-Validator里仅仅是告诉我们如何使用国际化,但是根据请求头的Accept-Language不通过的内容进行切换,它是没有实现的。带着这个疑问,决定看一下RestEasy的源码
根据RestEasy官方文档的Validaton部分,RestEasy集成Hibernate-Validator的工作是在resteasy-validator-provider 这个jar包下完成的。通过查看
MessageInterpolator该接口的实现类,发现对应的RestEasy实现类为LocaleSpecificMessageInterpolator
发现org.jboss.resteasy.plugins.validation.GeneralValidatorImpl#getValidator方法实例化了LocaleSpecificMessageInterpolator
getLocale里的逻辑就是根据Accept-Language请求头获得Locale,然后把这个Locale和当前的MessageInterpolator传进去,达到改变当前MessageInterpolator的locale的目的,这里RestEasy做的很聪明。目的只是想要改变locale即可,那么就用同一种MessageInterpolator类型的实现LocaleSpecificMessageInterpolator再把现有的MessageInterpolator wrap一下就好了,通过构造方法设置locale。
注意下面这两行很关键,我们知道Validator的实例是由ValidatorFactory生成的.上面已经根据不同locale生成了一个新的MessageInterpolator,那么现在需要ValidatorFactory使用新的MessageInterpolator生成Validator实例。
usingContext()方法就是干这个事情的
Bean Validation文档
ValidatorContext returned by usingContext() can be used to customize the state in which the Validator must be initialized. This is used to customize the MessageInterpolator, the TraversableResolver, the ParameterNameProvider, the ClockProvider or the ConstraintValidatorFactory
Hibernate-Validator文档
When working with a configured validator factory it can occasionally be required to apply a different configuration to a single Validator instance. Example 9.28, “Configuring a Validator instance via usingContext()” shows how this can be achieved by calling ValidatorFactory#usingContext()
3.获得Validator实例之后再调用对应的方法,其中一个截图
4. 那么GeneralValidatorImpl又是在哪里实例化的呢?查看其构造方法被调用的位置
根据上面两张截图,ValidatorContextResolver是一个Provider,实现了ContextResolver接口,所以在收到请求时会进入此Provider,从而完成实例化新的MessageInterpolator,并获取对应Validator接口实例,进而完成校验工作。有关ContextResolver的更多信息参考JAX-RS规范
因为项目中的CDI是使用Weld CDI,Weld CDI初始化过程中会加载inject的spi,由hiernate-validator-cdi里的spi触发
RestEasy同样会生成一个ValidatorFactory实例