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設定才更合理。