Spring:validate和messages消息源统一化

目录

1.源码解读-validator包

2. 源码解读-MessageSource

3.Spring-MessageSource与validator结合

4.实践

5.踩坑记录


前提:项目基于spring boot。

项目中,我们经常处理国际化消息及在校验中也需要抛出国际化消息,但是messages和validator的消息配置文件不一致,且处理器看起来也是不一样的,那么接下来以两者均需要国际化来看一下他们的处理的不同。

1.源码解读-validator包

首先看validator,其中有一个处理消息的概念:interpolator,即“篡改”,也就是对于消息可进行最终输出前的处理,如占位符替换等,否则将直接输出配置的内容。接着,该概念下,需要具体的配置加载:org.hibernate.validator.spi.resourceloading.ResourceBundleLocator,作用就是根据国际化环境(en或zh等)加载相应的资源加载器,具体有以下几个:

 org.hibernate.validator.resourceloading.AggregateResourceBundleLocator

 org.hibernate.validator.resourceloading.CachingResourceBundleLocator

 org.hibernate.validator.resourceloading.DelegatingResourceBundleLocator 

 org.hibernate.validator.resourceloading.PlatformResourceBundleLocator 

 org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator 

以上几个类(非加粗)实现是基于装饰者模式的,基础的是 PlatformResourceBundleLocator,原理通过name去classpath路径加载对应环境的资源文件,然后从内容里获取key的值,另外几个从名字上也可以看出加强了什么:Aggregate聚合,cache缓存,delegate代理。

可以看到,其实最终起作用的是:java.util.ResourceBundle,即本地资源文件。

在看加粗类,它是基于messages中MessageSource的,接下来会讲到。

2. 源码解读-MessageSource

然后我们来看专门处理国际化消息的org.springframework.context.MessageSource,它下边的实现有很多,基本是各种不同来源,有基于本地内存的(map)、java.util.ResourceBundle等,与上面相同的就是基于java.util.ResourceBundle的org.springframework.context.support.ResourceBundleMessageSource,这也是我们经常使用的。

3.Spring-MessageSource与validator结合

so,到此,两者可以联系起来了,但是,两者的默认资源名称是不同的。validator是ValidationMessages,message是messages。

所以,messages可以是validator的某个实现,也就是他俩是有交集的,当且仅当均使用同一个MessageSource,validator使用MessageSourceResourceBundleLocator时,可以通过messageSource进行统一控制。

4.实践

好了,那这样的话,我们统一也就很简单了。

看了这么多,配置起来其实很简单:

@Configuration
public class ValidatorConfig {
    @Autowired
    private MessageSource messageSource;
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;

    }
}

本例中,messageSource使用的是默认的,即message为basename,只要在文件中放几个key,即可测试,现提供出来

 控制器

@ResponseBody
@PostMapping("testValid")
public String testValid(@Valid User user){
    System.out.println(user.getName());
    return "success";
}

 实体类

@Data
public class User {
    @NotNull(message = "name不能为空")
    private String name;

    private String number;
    @Min(value = 100, message = "{age}{validation.message.min}")
    private int age;
}

马上去试试吧!

5.踩坑记录

上述改动后,基本已将项目中Spring框架下的国际化和validator功能统一,一般的使用基本没问题,使用默认的messageSource也没问题,问题出在哪儿呢,当我们自定义开发messageSource时,一些细节需要注意,否则会导致很大的问题。

5.1 多层嵌套参数不生效

问题描述

  • 发生条件:校验功能中类似@Length(min=1,max=10,message="{valid.length.msg}")嵌套注解&自定义messageSource,
  • 现象:配置值为valid.length.msg=长度应该处于{min}和{max},校验出错时预期输出:“长度应该处于1和10”,但是实际输出“长度应该处于和”,即第一层拿到了,但是min和max没找到
  • 原因:忽略的validator的另一个数据来源:本身配置值,我们仅想到了去国际化文件中获取
  • 解读:我们来简单做个假想,第二层参数不是我们数据源的数据,而是配置值,这点validator组件已经想到,所以类似于这种参数我们不会维护也不能维护,那么他怎么知道该读取自己配置呢?他需要我们怎么处理呢?这个时候,最好是我们告诉他这个值不是我维护的,我没有找到,在程序中的表现应该就是异常抛出了。

好,带着我们的假设,进入源码:

校验功能会进入以下方法进行参数的解析(解析大括号的参数)与读取(messageSource)

org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator#interpolateBundleMessage
源码如下:

private String interpolateBundleMessage(String message, ResourceBundle bundle, Locale locale, boolean recursive)
			throws MessageDescriptorFormatException {
		TokenCollector tokenCollector = new TokenCollector( message, InterpolationTermType.PARAMETER );
		TokenIterator tokenIterator = new TokenIterator( tokenCollector.getTokenList() );
		while ( tokenIterator.hasMoreInterpolationTerms() ) {
			String term = tokenIterator.nextInterpolationTerm();
			String resolvedParameterValue = resolveParameter(
					term, bundle, locale, recursive
			);
			tokenIterator.replaceCurrentInterpolationTerm( resolvedParameterValue );
		}
		return tokenIterator.getInterpolatedMessage();
	}

重点关注resolveParameter方法,我们看看他做了什么

private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, boolean recursive)
            throws MessageDescriptorFormatException {
        String parameterValue;
        try {
            if ( bundle != null ) {
                parameterValue = bundle.getString( removeCurlyBraces( parameterName ) );
                if ( recursive ) {
                    parameterValue = interpolateBundleMessage( parameterValue, bundle, locale, recursive );
                }
            }
            else {
                parameterValue = parameterName;
            }
        }
        catch (MissingResourceException e) {
            // return parameter itself
            parameterValue = parameterName;
        }
        return parameterValue;
    }


看没看到,有一个异常拦截,也就是获取不到资源时,他会捕获该异常,并且把没有拿到源数据的参数重新赋值回原样,供之后拿配置的信息赋值使用。
nice!
补坑尝试:
按上面的方式,我在自定义的messageSource中没有获取到值时,抛出MissingResourceException,完美解决!

 

附上语言环境配置:

Java的系统启动后,通过Locale.getDefault()能够得到一个当前应用默认的Locale信息,但如果希望我们的应用不管部署在任意机器上,可以保持同一个默认Locale怎么办呢?也就是如何修改Java启动的默认Locale。

1. 可以在启动的入口代码处增加以下语句

Locale.setDefault(newLocale("en","US"));
2. 在Java启动时增加以下参数:

-Duser.language=en -Duser.country=US

3. 修改操作系统的语言设置。

Windows: 控制面板 --> 地区语言

Linux: 永久方案 【vi /etc/sysconfig/i18n 修改如下LANG="en_US.UTF-8" 】

          临时方案 【export LANG=en_US.UTF-8】

你可能感兴趣的:(java,web,Spring,设计模式,validator,spring,messages)