Hibernate Validator框架支持验证信息的参数化。
以Length注解为例:
public class User { private String name; @Length(message="{MSG_W00001}",min=1,max=10) public String getName(){ return name; } }
配置信息如下所示:
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="messages/message" /> <property name="useCodeAsDefaultMessage" value="true" /> </bean> <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> <property name="validationMessageSource" ref="messageSource"/> </bean>
MessageResource:
MSG_W00001 = length must be between {min} and {max}
当name不满足Length验证时,期待的验证信息应该是:
length must be between 1 and 10
但实际上验证信息却是:
length must be between min and max
网上搜索一番也没找到答案,只好自己动手解决问题。在Hibernate Validator(ver 4.3.2)默认是依赖ResourceBundleMessageInterpolator类来获得最终的验证信息,核心函数如下:
private String interpolateMessage(String message, Map<String, Object> annotationParameters, Locale locale) { LocalisedMessage localisedMessage = new LocalisedMessage( message, locale ); String resolvedMessage = null; if ( cacheMessages ) { resolvedMessage = resolvedMessages.get( localisedMessage ); } // if the message is not already in the cache we have to run
//step 1-3 of the message resolution if ( resolvedMessage == null ) { ResourceBundle userResourceBundle = userResourceBundleLocator .getResourceBundle( locale ); ResourceBundle defaultResourceBundle = defaultResourceBundleLocator .getResourceBundle( locale ); String userBundleResolvedMessage; resolvedMessage = message; boolean evaluatedDefaultBundleOnce = false; do { // search the user bundle recursive (step1) userBundleResolvedMessage = replaceVariables( resolvedMessage, userResourceBundle, locale, true ); // exit condition - we have at least tried to validate
//against the default bundle and there was no // further replacements if ( evaluatedDefaultBundleOnce && !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) { break; } // search the default bundle non recursive (step2) resolvedMessage = replaceVariables( userBundleResolvedMessage,
defaultResourceBundle, locale, false ); evaluatedDefaultBundleOnce = true; } while ( true ); } // cache resolved message if ( cacheMessages ) { String cachedResolvedMessage =
resolvedMessages.putIfAbsent( localisedMessage, resolvedMessage ); if ( cachedResolvedMessage != null ) { resolvedMessage = cachedResolvedMessage; } } // resolve annotation attributes (step 4) resolvedMessage = replaceAnnotationAttributes( resolvedMessage, annotationParameters ); // last but not least we have to take care of escaped literals resolvedMessage = resolvedMessage.replace( "\\{", "{" ); resolvedMessage = resolvedMessage.replace( "\\}", "}" ); resolvedMessage = resolvedMessage.replace( "\\\\", "\\" ); return resolvedMessage; }
可以看到step 4正是我们期待的把{min}和{max}替换成Length里min和max的处理,可是为什么不起作用呢?
理论上验证信息应该经历以下变化:
{MSG_W00001} -> length must be between {min} and {max} -> length must be between 1 and 10
可是通过调试却发现在step 4处理前验证信息变成了下面的样子:
length must be between min and max
这说明step1-3的处理有问题。具体来看step1-3的处理主要通过replaceVariables和resolveParameter方法来实现:
private String replaceVariables(String message, ResourceBundle bundle, Locale locale, boolean recurse) { Matcher matcher = MESSAGE_PARAMETER_PATTERN.matcher( message ); StringBuffer sb = new StringBuffer(); String resolvedParameterValue; while ( matcher.find() ) { String parameter = matcher.group( 1 ); resolvedParameterValue = resolveParameter( parameter, bundle, locale, recurse ); matcher.appendReplacement( sb, Matcher.quoteReplacement( resolvedParameterValue ) ); } matcher.appendTail( sb ); return sb.toString(); } private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, boolean recurse) { String parameterValue; try { if ( bundle != null ) { parameterValue = bundle.getString( removeCurlyBrace( parameterName ) ); if ( recurse ) { parameterValue = replaceVariables( parameterValue, bundle, locale, recurse ); } } else { parameterValue = parameterName; } } catch ( MissingResourceException e ) { // return parameter itself parameterValue = parameterName; } return parameterValue; }
resolveParameter方法里,recurse被设置为ture,所以会递归的查找和替换参数。
首先{MSG_W00001}把替换成length must be between {min} and {max}。
然后会继续在MessageResource中查找min和max。根据resolveParameter方法如果找不到则会被替换。显然我们
在MessageResource没有定义min和max,但是问什么{min}和{max}会被替换成min和max呢?
问题出在<property name="useCodeAsDefaultMessage" value="true" />这个设置上,当找不到min和max对应的值时bundle.getString函数
返回了它的参数,而不是抛出MissingResourceException异常,导致{min}和{max}会被替换成min和max。
把配置修改成<property name="useCodeAsDefaultMessage" value="false" />,验证信息就变成了我们所期待的"length must be between 1 and 10"。
至此这个问题就解决了。可是感觉Hibernate Validator在这个问题上处理有很问题,很难会让人联想到useCodeAsDefaultMessage这个设定会
对验证信息的参数化造成影响。ResourceBundleMessageInterpolator应该不依赖于全局的useCodeAsDefaultMessage设定才更合理。