文章目录
- 学习建议
- 全部的约束注解
- 关于@NotEmpty和@NotBlank的注意事项
- 关于@Size的注意事项
- 约束注解可以放的位置
- JavaBean
- Validator接口
- 约束注解的继承性
- 普通方法和构造方法入参和返回值
- ExecutableValidator 接口
- 约束注解的继承性
- 错误信息国际化显示
- 定义message使用的消息key
- 定义ValidationMessages文件
- 验证错误消息国际化是否生效
- RestEasy是如何帮我们做到的呢?
- Hibernate-Validator初始化过程
- 备注
学习建议
建议结合Hibernate-Validator和Jakarta Bean Validation规范学习。
如果忘记某个注解或功能如何使用了,可以快速参考Hibernate-Validator官方提供的示例
官方代码非常清晰,以每一章为一个目录
全部的约束注解
可以从jakarta-bean-validation-spec-3.0规范中找到全部注解以及约束条件
关于@NotEmpty和@NotBlank的注意事项
- 二者都会校验是否为null,所以不需要再次添加@NotNull
关于@Size的注意事项
- 此注解是针对字符串和集合、数组、Map类型的,对于数值类型的校验使用@Max和@Min
约束注解可以放的位置
JavaBean
-
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,即约束注解是放在类上的,而不是具体的属性上的
Validator接口
以上对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 接口
对普通方法、构造方法的入参和返回值的校验由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文件,
- message_zh_CN.properties
- message_en_US.properties
Hibernate-Validator也是使用了相同的方法,不过i18n配置文件的前缀需要使用ValidationMessages,为什么呢?因为这是Bean Validation规范里定的。
如果你就不想使用ValidationMessages这个名字,那么你也可以自己实现MessageInterpolator。具体实现的细节可以参考Hibernate-Validator官方文档和Bean Validation规范文档。这一部分不在本篇文章里做过多讨论
定义message使用的消息key
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文件
这里定义两个文件ValidationMessages_zh_CN.properties和ValidationMessages_en_US.properties来做测试。二者均放在resource目录下
- ValidationMessages_en_US.properties的内容
userName.invalid= userName ${validatedValue} is invalid userAge.invalid= max value is {value}, but current value is ${validatedValue}
- ValidationMessages_zh_CN.properties的内容
汉字部分需要转成UnicodeuserName.invalid=\u7528\u6237\u540d ${validatedValue} \u662f\u975e\u6cd5\u7684 userAge.invalid=\u6700\u5927\u503c\u662f {value}, \u4f46\u662f\u5f53\u524d\u503c\u662f ${validatedValue}
在每个key后面的实际内容中,上述例子有两个特殊值,
- ${validatedValue}代表的是当前值,如果要读取当前值,必须是前面这个变量名。里面也可以写EL表达式
- {value} 是具体约束注解的属性,可能是value,也可能是min,max这种
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的请求头来国际化错误信息的
RestEasy是如何帮我们做到的呢?
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规范
Hibernate-Validator初始化过程
因为项目中的CDI是使用Weld CDI,Weld CDI初始化过程中会加载inject的spi,由hiernate-validator-cdi里的spi触发
RestEasy同样会生成一个ValidatorFactory实例
备注
- 源码仓库