0. 背景介绍

       输入验证pring 处理的最重要 Web 开发任务之一,在 Spring MVC中有两种方式可以验证输入:一种是Spring 自带的验证框架,另外一种是利用JSR实现,JSR验证比Spring⾃带的验证器使用起来⽅便很多。JSR 是一个规范文档,指定了一整套API,通过标注给对象属性添加约束。Hibernate Validator 就是 JSR 规范的具体实现, Hibernate Validator 提供了 JSR 规范中所有内置约束注解的实现,以及一些附加的约束注解,除此之外用户还可以自定义约束注解。

      然而 Hibernate Validator 提供的接口没有直接支持输出本地化的错误验证消息。本文结合项目实践,总结了如何对内置的和⽤户⾃定义的约束注解提供本地化支持,以及如何从用户自定义的资源文件中读取本地化的错误 验证消息。但现在play框架下不支持 Hibernate Validator的国际化,因此需要对play框架下的 Hibernate Validator进⾏重构。

1. Hibernate Validator 概述

       在Java应⽤程序中,必须要对输入进来的数据从语义上分析是有效的,也就是数据校验。对数据的校验是一项 贯穿于从表示层到持久化层的常见任务,通常在每个层中都需要做相同的验证逻辑,然而不同的层通常有不同的开发人员做编码,这样就会存在冗余代码以及语义一致性等代码管理上的问题。

      为了避免重复验证以及管理问题,开发⼈员经常将验证逻辑与相应的域模型进行捆绑。JSR 349 Bean 验证 1.1 是一个数据验证的规范,为 Java Bean 验证定义了相应的元数据模型和接口,默认的元数据是 Java 注释,通过使用 XML 对原有的元数据进行覆盖和扩展。在 Java 应⽤程序中,通过使⽤ Bean 验证自带的约束或者⽤户自定义的约束验证来确保 Java Bean 的正确性。Bean 验证是一个 runtime 的数据验证框架,如果验证失败, 错误信息会⽴马返回。从而使验证逻辑从业务代码中分离出来。

     Hibernate Validator 是 对 JSR 349 验证规范的具体实现,提供了了 JSR 规范中所有内置约束注解的实现,以及 ⼀一些附加的约束注解。

2. Hibernate Validator 对全球化⽀支持概述

      世界经济日益全球化的同时软件国际化势在必然,当一个软件或者应用需要在全球范围内使用的时候,最简单的要求就是界⾯上的信息能用本地化的语言来显示。然而 Hibernate Validator 4.0 提供的接⼝对全球化支持存在下面两个问题:

问题 1:
          只能显示英文的错误消息,不能读取翻译的错误验证消息。因为默认的消息解释器(message interpolator)使用的是 JVM 默认的 locale(Locale.getDafult()),通常情况下为英文;

问题 2:

         Hibernate Validator 验证过程中的失败消息默认是从类路径下的资源文件 ValidationMessage.properties 读 取,然而在实际项⽬中,通常会根据模块结构⾃定义资源文件名称,⽅便源代码的管理以及资源文件的重用。

3. ⾃自定义约束注解提供本地化⽀支持

⾃自定义注解

        自定义约束注解就是用户根据⾃己的需要重新定义一个新的约束注解,通常包括两部分,⼀是约束注解的定义,二是约束验证器。

约束注解

        约束注解就是对某一⽅法、字段、属性或其组合形式等进行约束的注解,通常包含以下几个部分:

@Target({ }):约束注解应用的⽬标元素类型,METHOD(约束相关的 getter 方法), FIELD(约束属性), TYPE(约束 java bean), ANNOTATION_TYPE(⽤在组合约束中), CONSTRUCTOR(对构造函数的约束), PARAMETER(对参数的约束)。
@Retention():约束注解应⽤的时机,⽐如在应用程序运行时进行约束
@Constraint(validatedBy ={}):与约束注解关联的验证器,每个约束注解都对应一个验证器
String message() default " ":约束注解验证失败时的输出消息;Class<?>[] groups() default { };:约束注解在验证时所属的组别
Class<? extends Payload>[] payload() default { };:约束注解的有效负载

4. Hibernate Validator校验⽅方式在Play框架下的尝试规律律总结

(1)⼤前提:

@DecimalMax(MAX_BIT_COIN_QUANTITY,inclusive=)设定边界值String MAX_BIT_COIN_QUANTITY = "21000000";
Hibernate Validator校验⽅式默认从 ValidationMessages.properties:javax.validation.constraints.DecimalMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}中获值

情况一:inclusive默认为true,输入:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["必须⼩小于或等于 21000000"]}}

情况⼆:inclusive默认为true,输入:quantity:"21000000"

输出:

{"success":true,"code":0,"message":"Ok"}

情况三:inclusive为false,输入:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["必须⼩小于或等于 21000000"]}}

情况四:inclusive为false,输入:quantity:"21000000"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["必须⼩小于或等于 21000000"]}}

结论:⽆论请求头的语言变为什么,都是输出中文,因为是从本地的JVM中拿的语言。

(2)⼤前提:message.en⽂件内容:error.decimal.max=must be less than {0}

@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max",inclusive=)

情况一:inclusive默认为true,输⼊:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than true"]}}

情况二:inclusive默认为true,输入:quantity:"21000000"

输出:

{"success":true,"code":0,"message":"Ok"}

情况三:inclusive为false,输入:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than false"]}}

情况四:inclusive为false,输⼊:quantity:"21000000"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than false"]}}

结论:若inclusive为true时,最大值校验包含边界值,大于边界值时会在错误信息中返true; 若inclusive 为false时,最大值校验不包含边界值,⼤于或等于边界值时会在错误信息中返false。

(3)⼤前提:message.en文件内容:error.decimal.max=must be less than {1}

@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max",inclusive=)

情况一:inclusive默认为true,输入:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than 21000000"]}}

情况二:inclusive默认为true,输入:quantity:"21000000"

输出:

{"success":true,"code":0,"message":"Ok"}

情况三:inclusive为false,输入:quantity:"21000001"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than 21000000"]}}

情况四:inclusive为false,输入:quantity:"21000000"

输出:

{"success":false,"code":2,"message":"Parameter error","data":{"quantity":["must be less than 21000000"]}}

结论: 若inclusive为true时,最大值校验包含边界值,⼤于边界值时会在错误信息中返边界值 若inclusive 为false时,最大值校验不包含边界值,大于或等于边界值时会在错误信息中返边界值

5. Hibernate Validator ⼯作原理

使用javax.validation.MessageInterpolator来解析消息

package javax.validation;

import java.util.Locale;
import javax.validation.metadata.ConstraintDescriptor;

public interface MessageInterpolator {
String interpolate(String var1, MessageInterpolator.Context var2);

String interpolate(String var1, MessageInterpolator.Context var2, Locale var3);

public interface Context {
ConstraintDescriptor<?> getConstraintDescriptor();
Object getValidatedValue();
<T> T unwrap(Class<T> var1);
}
}

即此时使用的hibernate实现,注⼊了spring的messageSource来解析消息时:

public void setValidationMessageSource(MessageSource messageSource) {

this.messageInterpolator = HibernateValidatorDelegate.buildMessageInterpolator(messageSource);

}

/**
* Inner class to avoid a hard-coded Hibernate Validator 4.1+ dependency. */

private static class HibernateValidatorDelegate {
public static MessageInterpolator buildMessageInterpolator(MessageSource
messageSource) {
return new ResourceBundleMessageInterpolator(new
MessageSourceResourceBundleLocator(messageSource));
} }

即内部委托给了

org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator#ResourceBundleMessageIn terpolator,并使用如下代码解析消息:

public String interpolate(String message, Context context) {
// probably no need for caching, but it could be done by parameters since the map
// is immutable and uniquely built per Validation definition, the comparison has
to be based on == and not equals though
return interpolateMessage( message,
context.getConstraintDescriptor().getAttributes(), defaultLocale ); }

此处可以看到context.getConstraintDescriptor().getAttributes(),其作用是获取到注解如@Length上的所有数据,具体代码实现如下:

private Map<String, Object> buildAnnotationParameterMap(Annotation annotation) { final Method[] declaredMethods = ReflectionHelper.getDeclaredMethods(

annotation.annotationType() );
Map<String, Object> parameters = new HashMap<String, Object>(

declaredMethods.length );
for ( Method m : declaredMethods ) {

try {
parameters.put( m.getName(), m.invoke( annotation ) );

}
catch ( IllegalAccessException e ) {
throw log.getUnableToReadAnnotationAttributesException( annotation.getClass(), e );

}
catch ( InvocationTargetException e ) {
throw log.getUnableToReadAnnotationAttributesException( annotation.getClass(), e );

} }

return Collections.unmodifiableMap( parameters ); }

循环每⼀个方法 并获取值放入map,接着进入方法:

private String interpolateMessage(String message, Map<String, Object>  annotationParameters, Locale locale)

6. Hibernate Validator 使用用户⾃定义的资源文件

      文章开头提到 Hibernate Validator 验证过程中第二个问题是验证失败的错误消息默认从类路径下的ValidationMessage.properties 中读取,但是很多时候我们希望从自定义的资源文件(resource bundle)文件中读取,而不是指定路径下的ValidationMessage.properties,以便于翻译以及重用已有的资源文件。在这种情况下,Hibernate Validator 的资源包定位器ResourceBundleLocator 可以解决这个问题。

     Hibernate Validator 中的默认资源文件解析(ResourceBundleMessageInterpolator)将解析资源文件和检索相应的错误消息委派给资源包定位器 ResourceBundleMessageInterpolator。如果要使用用户⾃定义的资源⽂件,只需要将用户⾃定义资源文件名作为参数传递给资源包定位器 PlatformResourceBundleLocator,在启用 ValidatorFactory 的时候将新的资源包定位器示例作为参数传递给 ValidatorFactory。

7. 适⽤用于Play框架下注解的国际化改造

@DecimalMax

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })

public @interface DecimalMax {
String message() default "{javax.validation.constraints.DecimalMax.message}"; Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };

/**
* The {@code String} representation of the max value according to the * {@code BigDecimal} string representation.
*
* @return value the element must be lower or equal to
*/

String value();
/**

* Specifies whether the specified maximum is inclusive or exclusive.

* By default, it is inclusive. *

* @return {@code true} if the value must be lower or equal to the specified
maximum,
* {@code false} if the value must be lower *
* @since 1.1
*/

boolean inclusive() default true;
/**
* Defines several {@link DecimalMax} annotations on the same element. *
* @see DecimalMax
*/

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME)
@Documented
@interface List {

DecimalMax[] value();
} }

操作参数简介

 

修饰符和类型

操作参数

描述

String

value(必须 参数)

根据BigDecimal的最大值用String类型的字符串表示,要求值必须⼩于或等于最大限制值

Class<?>[]

groups

default为{}

boolean

inclusive

指定是否包含最大值,值必须小于等于(inclusive=true)/小于

(inclusive=false)value属性指定的值。

String

message

default为 "{javax.validation.constraints.DecimalMax.message}"

Class<? extends Payload>[]

payload

default为{}

 

 

 

 

 

 

 

注:String message() default "{javax.validation.constraints.DecimalMax.message}"会从指定路径下 ValidationMessage.properties拿⽂件,默认的消息解释器(message interpolator)使⽤的是 JVM 默认的 locale(Locale.getDafult())会从本地JVM中拿语言,而不能随Play框架下的语言的改变⽽改变,故必须重构Hibernate Validator 验证方式注解中的message。

实现步骤

第一步:添加一个messge的key值

@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max")

第二步:在对应语言版本的message⽂文件中添加key对应的value值

error.decimal.max=must be less than {1}

注:{1}⽽不是{0},{0}默认inclusive为true,{1}value属性指定的值。因为原⽣的文件有这样一个限制: javax.validation.constraints.DecimalMax.message = must be less than ${inclusive == true ? 'or equal to ' : ''}{value}

inclusive用法

@DecimalMax(value=,inclusive=) | 值必须小于等于(inclusive=true)/小于(inclusive=false)value属性指定的值。

例1:

String MAX_BIT_COIN_QUANTITY = "21000000";
@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max") protected BigDecimal quantity;

@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max",inclusive = true)
protected BigDecimal quantity;

注意:表示输入最⼤的数量必须小于或等于21000000

例2:

@DecimalMax(value = MAX_BIT_COIN_QUANTITY, message = "error.decimal.max",inclusive = false)
protected BigDecimal quantity;

注:表示输⼊最大的数量必须小于21000000,但不包括21000000! Hibernate Validator常⽤参数校验注解

Hibernate Validator常⽤参数校验注解

Hibernate Validator校验注解

说明

@NotNull

值不能为空

@Null

值必须为空

@Pattern(regex=)

字符串必须匹配正则表达式

@Size(min=, max=)

集合的元素数量必须在min和max之间

@CreditCardNumber(ignoreNonDigitCharacters=)

字符串必须是信用卡号(按美国的标准验的-_-!)

@Email

字符串必须是Email地址

@Length(min=,max=)

检查字符的长度

@NotBlank

字符串必须有字符

@NotEmpty

字符串不为null,集合有元素

@Range(min=,max=)

数字必须大于等于min,⼩于等于max

@SafeHtml

字符串是安全的html

@URL

字符串是合法的URL

@AssertFalse

值必须是false

@AssertTrue

值必须是true

@DecimalMax(value=,inclusive=)

值必须小于等于(inclusive=true)/⼩于 (inclusive=false)value属性指定的值。可注解在字符串类型的属性上

@DecimalMin()

值必须大于等于(inclusive=true)/大于 (inclusive=false)value属性指定的值。可注解在字符串类型的属性上

@Digits(integer=,fraction=)

数字格式检查,integer指定整数部分的最大⻓长度, fraction指定小数部分的最⼤长度

@Future

值必须是未来的日期

@Past

值必须是过去的日期

@Max(value=)

值必须小于等于value指定的值,不能注解在字符串类型的属性上

@Min(value=)

值必须大于等于value指定的值,不能注解在字符串类型的属性上