记录一个使用Hibernate Validator验证信息参数化遇到的问题

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值