热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《SpringBoot手册》:国际化组件MessageSource

你好,我是看山。咱们今天一起来聊聊SpringBoot中的国际化组件MessageSource。初识MessageSource先看一下类图:从类图可

《SpringBoot 手册》:国际化组件 MessageSource

你好,我是看山。

咱们今天一起来聊聊 SpringBoot 中的国际化组件 MessageSource。

初识 MessageSource

先看一下类图:

MessageSource 类图

从类图可以看到,Spring 内置的MessageSource有三个实现类:

  • ResourceBundleMessageSource:通过 JDK 提供的 ResourceBundle 加载资源文件;
  • ReloadableResourceBundleMessageSource:通过 PropertiesPersister 加载资源,支持 xml、properties 两个格式,优先加载 properties 格式的文件。如果同时存在 properties 和 xml 的文件,会只加载 properties 的内容;
  • StaticMessageSource:是手动注入国际化内容,相当于手写代码。因为比较简单,而且实际用处不大,所以暂时不做讨论。

在 SpringBoot 中,默认创建 ResourceBundleMessageSource 实例实现国际化输出。标准的配置通过MessageSourceProperties类注入:

  • basename:加载资源的文件名,可以多个资源名称,通过逗号隔开,默认是“messages”;
  • encoding:加载文件的字符集,默认是 UTF-8,这个不多说;
  • cacheDuration:文件加载到内存后缓存时间,默认单位是秒。如果没有设置,只会加载一次缓存,不会自动更新。这个参数在 ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 稍微有些差异,会具体说下。
  • fallbackToSystemLocale:这是一个兜底开关。默认情况下,如果在指定语言中找不到对应的值,会从 basename 参数(默认是 messages.properties)中查找,如果再找不到可能直接返回或抛错。该参数设置为 true 的话,还会再走一步兜底逻辑,从当前系统语言对应配置文件中查找。该参数默认是 true;
  • alwaysUseMessageFormat:MessageSource 组件通过MessageFormat.format函数对国际化信息格式化,如果注入参数,输出结果是经过格式化的。比如MessageFormat.format("Hello, {0}!", "Kanshan")输出结果是“Hello, Kanshan!”。该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format函数对结果进行格式化,默认是 false;
  • useCodeAsDefaultMessage:当没有找到对应信息的时候,是否返回 code。也就是当找了所有能找的配置文件后,还是没有找到对应的信息,是否直接返回 code 值。默认是 false,即不返回 code,抛出NoSuchMessageException异常。

小试牛刀

从上面我们知道了一些简单的配置,但是还是没有办法知道 MessageSource 到底是什么,本节我们举个例子小试牛刀。

首先从https://start.spring.io/创建一个最少依赖spring-boot-starter-web的 SpringBoot 项目。

然后在 resources 目录下定义一组国际化配置文件,我们这里使用默认配置,所以 basename 是 messages:

## messages.properties
message.code1=[DEFAULT]code one
message.code2=[DEFAULT]code two
message.code3=[DEFAULT]code three
message.code4=[DEFAULT]code four
message.code5=[DEFAULT]code five
message.code6=[DEFAULT]code six## messages_en.properties
message.code2=[en]code two## messages_en_US.properties
message.code3=[en_US]code three## messages_zh.properties
message.code4=[中文] 丁字号## messages_zh_CN.properties
message.code5=[大陆区域中文] 戊字号## messages_zh_Hans.properties
message.code6=[简体中文] 己字号

一个定义了六个配置文件:

  • messages.properties:默认配置文件
  • messages_en.properties:英文配置文件
  • messages_en_US.properties:英文美国配置文件
  • messages_zh.properties:中文配置文件
  • messages_zh_CN.properties:中文中国大陆区域配置文件
  • messages_zh_Hans.properties:简体中文配置文件

从上面配置文件的命名可以看出,都是以 basename 开头,后面跟上语系和地区,三个参数以下划线分隔。

可以支持的语言和国家可以从java.util.Locale查找。

最后我们定义一个 Controller 实验:

@RestController
public class HelloController {&#64;Autowiredprivate MessageSource messageSource;&#64;GetMapping("m1")public List<String> m1(Locale locale) {final List<String> multi &#61; new ArrayList<>();multi.add(messageSource.getMessage("message.code1", null, locale));multi.add(messageSource.getMessage("message.code2", null, locale));multi.add(messageSource.getMessage("message.code3", null, locale));multi.add(messageSource.getMessage("message.code4", null, locale));multi.add(messageSource.getMessage("message.code5", null, locale));multi.add(messageSource.getMessage("message.code6", null, locale));return multi;}
}

我们通过不同的请求查看结果&#xff1a;

### 默认
GET http://localhost:8080/m1
### 结果是&#xff1a;
["[DEFAULT]code one","[DEFAULT]code two","[DEFAULT]code three","[中文] 丁字号","[大陆区域中文] 戊字号","[简体中文] 己字号"
]### local: en
GET http://localhost:8080/m1
Accept-Language: en
### 结果是&#xff1a;
["[DEFAULT]code one","[en]code two","[DEFAULT]code three","[DEFAULT]code four","[DEFAULT]code five","[DEFAULT]code six"
]### local: en-US
GET http://localhost:8080/m1
Accept-Language: en-US
### 结果是&#xff1a;
["[DEFAULT]code one","[en]code two","[en_US]code three","[DEFAULT]code four","[DEFAULT]code five","[DEFAULT]code six"
]### local: zh
GET http://localhost:8080/m1
Accept-Language: zh
### 结果是&#xff1a;
["[DEFAULT]code one","[DEFAULT]code two","[DEFAULT]code three","[中文] 丁字号","[DEFAULT]code five","[DEFAULT]code six"
]### local: zh-CN
GET http://localhost:8080/m1
Accept-Language: zh-CN
### 结果是&#xff1a;
["[DEFAULT]code one","[DEFAULT]code two","[DEFAULT]code three","[中文] 丁字号","[大陆区域中文] 戊字号","[DEFAULT]code six"
]

从上面的结果可以看出&#xff1a;

  1. 默认情况下&#xff0c;HTTP 请求没有传语言&#xff0c;所以使用了系统语言组装&#xff0c;相当于传参是zh-Hans&#xff0c;所以结果是简体中文优先&#xff1b;
  2. HTTP 请求定义的语言越精确&#xff0c;匹配的内容越精确&#xff1b;
  3. 默认情况下&#xff0c;指定语言配置文件找不到&#xff0c;会一次向上查找&#xff0c;地区 > 国家 > 语言 > 默认。

带参数的国际化信息

我们在 message.properties 中添加一行配置&#xff1a;

message.multiVars&#61;var1&#61;{0}, var2&#61;{1}

在刚才的 Controller 中增加一个请求&#xff1a;

&#64;GetMapping("m2")
public List<String> m2(Locale locale) {final List<String> multi &#61; new ArrayList<>();multi.add("参数为 null: " &#43; messageSource.getMessage("message.multiVars", null, locale));multi.add("参数为空&#xff1a;" &#43; messageSource.getMessage("message.multiVars", new Object[]{}, locale));multi.add("只传一个参数&#xff1a;" &#43; messageSource.getMessage("message.multiVars", new Object[]{"第一个参数"}, locale));multi.add("传两个参数&#xff1a;" &#43; messageSource.getMessage("message.multiVars", new Object[]{"第一个参数", "第二个参数"}, locale));multi.add("传超过两个参数&#xff1a;" &#43; messageSource.getMessage("message.multiVars", new Object[]{"第一个参数", "第二个参数", "第三个参数"}, locale));return multi;
}

我们看看结果&#xff1a;

###
GET http://localhost:8080/m2
### 结果是&#xff1a;
["参数为 null: var1&#61;{0}, var2&#61;{1}","参数为空&#xff1a;var1&#61;{0}, var2&#61;{1}","只传一个参数&#xff1a;var1&#61;第一个参数&#xff0c;var2&#61;{1}","传两个参数&#xff1a;var1&#61;第一个参数&#xff0c;var2&#61;第二个参数","传超过两个参数&#xff1a;var1&#61;第一个参数&#xff0c;var2&#61;第二个参数"
]

我们可以看到&#xff0c;我们在配置文件中定义了带参数的配置信息&#xff0c;此时&#xff0c;我们可以不传参数、传少于指定数量的参数、传符合指定数量的参数、传超过指定数量的参数&#xff0c;都可以正常返回国际化信息。

此处可以理解为&#xff0c;MessageFormat.format执行过程是for-index循环&#xff0c;从配置值中找格式为{数字}的占位符&#xff0c;然后用对应下标的输入参数替换&#xff0c;如果属于参数没了&#xff0c;就保持原样。

找不到配置内容

如果我们的配置文件中没有配置或者对应语言及其父级都没有配置呢&#xff1f;

这个就要靠前面说的useCodeAsDefaultMessage配置了&#xff0c;如果为 true&#xff0c;就会返回输入的 code&#xff0c;如果为 false&#xff0c;就会抛出异常。默认是 false&#xff0c;所以如果找不到会抛异常。比如&#xff1a;

&#64;GetMapping("m3")
public List<String> m3(Locale locale) {final List<String> multi &#61; new ArrayList<>();multi.add("不存在的 code: " &#43; messageSource.getMessage("message.notExist", null, locale));return multi;
}

这个时候我们执行 http 请求&#xff1a;

###
GET http://localhost:8080/m3
### 结果是&#xff1a;
{"timestamp": "2022-06-19T09:14:14.977&#43;00:00","status": 500,"error": "Internal Server Error","path": "/m3"
}

这是报错了&#xff0c;异常栈是&#xff1a;

org.springframework.context.NoSuchMessageException: No message found under code &#39;message.notExist&#39; for locale &#39;zh_CN_#Hans&#39;.at org.springframework.context.support.AbstractMessageSource.getMessage(AbstractMessageSource.java:161) ~[spring-context-5.3.20.jar:5.3.20]at cn.howardliu.effective.spring.springbootmessages.controller.HelloController.m3(HelloController.java:47) ~[classes/:na]……此处省略

自定义 MessageSource

本文开头说过&#xff0c;MessageSource 有三种实现&#xff0c;Spring 默认使用了 ResourceBundleMessageSource&#xff0c;我们可以自定义使用 ReloadableResourceBundleMessageSource。

既然是在 SpringBoot 中&#xff0c;我们可以依靠 SpringBoot 的特性定义&#xff1a;

&#64;Configuration(proxyBeanMethods &#61; false)
&#64;ConditionalOnMissingBean(name &#61; AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search &#61; SearchStrategy.CURRENT)
&#64;ConditionalOnProperty(name &#61; "spring.messages-type", havingValue &#61; "ReloadableResourceBundleMessageSource")
&#64;AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
&#64;Conditional(ReloadResourceBundleCondition.class)
&#64;EnableConfigurationProperties
public class ReloadMessageSourceAutoConfiguration {private static final Resource[] NO_RESOURCES &#61; {};&#64;Bean&#64;ConfigurationProperties(prefix &#61; "spring.messages")public MessageSourceProperties messageSourceProperties() {return new MessageSourceProperties();}&#64;Beanpublic MessageSource messageSource(MessageSourceProperties properties) {ReloadableResourceBundleMessageSource messageSource &#61; new ReloadableResourceBundleMessageSource();if (StringUtils.hasText(properties.getBasename())) {final String[] originBaseNames &#61; StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename()));final String[] baseNames &#61; new String[originBaseNames.length];for (int i &#61; 0; i < originBaseNames.length; i&#43;&#43;) {if (originBaseNames[i].startsWith("classpath:")) {baseNames[i] &#61; originBaseNames[i];} else {baseNames[i] &#61; "classpath:" &#43; originBaseNames[i];}}messageSource.setBasenames(baseNames);}if (properties.getEncoding() !&#61; null) {messageSource.setDefaultEncoding(properties.getEncoding().name());}messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());Duration cacheDuration &#61; properties.getCacheDuration();if (cacheDuration !&#61; null) {messageSource.setCacheMillis(cacheDuration.toMillis());}messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());return messageSource;}protected static class ReloadResourceBundleCondition extends SpringBootCondition {private static final ConcurrentReferenceHashMap<String, ConditionOutcome> CACHE &#61;new ConcurrentReferenceHashMap<>();&#64;Overridepublic ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {String basename &#61; context.getEnvironment().getProperty("spring.messages.basename", "messages");ConditionOutcome outcome &#61; CACHE.get(basename);if (outcome &#61;&#61; null) {outcome &#61; getMatchOutcomeForBasename(context, basename);CACHE.put(basename, outcome);}return outcome;}private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {ConditionMessage.Builder message &#61; ConditionMessage.forCondition("ResourceBundle");for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {for (Resource resource : getResources(context.getClassLoader(), name)) {if (resource.exists()) {return ConditionOutcome.match(message.found("bundle").items(resource));}}}return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " &#43; basename).atAll());}private Resource[] getResources(ClassLoader classLoader, String name) {String target &#61; name.replace(&#39;.&#39;, &#39;/&#39;);try {return new PathMatchingResourcePatternResolver(classLoader).getResources("classpath*:" &#43; target &#43; ".properties");} catch (Exception ex) {return NO_RESOURCES;}}}}

我们可以看到&#xff0c;我们在执行messageSource.setBasenames(baseNames);的时候&#xff0c;baseNames中的值都是设置成classpath:开头的&#xff0c;这是为了使ReloadableResourceBundleMessageSource能够读取 CLASSPATH 下的配置文件。当然也可以使用绝对路径或者相对路径实现&#xff0c;这个是比较灵活的。

我们可以通过修改配置文件内容&#xff0c;查看变化&#xff0c;这里就不再赘述。纸上得来终觉浅&#xff0c;绝知此事要躬行。

文末总结

本文通过几个小例子介绍了MessageSource的使用。这里做一下预告&#xff0c;下一章我们会从源码角度分析MessageSource 的实现类ResourceBundleMessageSourceReloadableResourceBundleMessageSource的执行逻辑&#xff1b;然后我们自定义扩展&#xff0c;从 Nacos 中读取配置内容&#xff0c;实现更加灵活的配置。

本文中的实例已经传到 GitHub&#xff0c;关注公众号「看山的小屋」&#xff0c;回复spring获取源码。

青山不改&#xff0c;绿水长流&#xff0c;我们下次见。

推荐阅读


  • SpringBoot 实战&#xff1a;一招实现结果的优雅响应
  • SpringBoot 实战&#xff1a;如何优雅的处理异常
  • SpringBoot 实战&#xff1a;通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战&#xff1a;自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战&#xff1a;优雅的使用枚举参数
  • SpringBoot 实战&#xff1a;优雅的使用枚举参数&#xff08;原理篇&#xff09;
  • SpringBoot 实战&#xff1a;在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战&#xff1a;在 RequestBody 中优雅的使用枚举参数&#xff08;原理篇&#xff09;
  • SpringBoot 实战&#xff1a;JUnit5&#43;MockMvc&#43;Mockito 做好单元测试
  • SpringBoot 实战&#xff1a;加载和读取资源文件内容
  • 《SpringBoot 手册》&#xff1a;国际化组件 MessageSource


你好&#xff0c;我是看山。游于码界&#xff0c;戏享人生。如果文章对您有帮助&#xff0c;请点赞、收藏、关注。我还整理了一些精品学习资料&#xff0c;关注公众号「看山的小屋」&#xff0c;回复“资料”即可获得。

个人主页&#xff1a;https://www.howardliu.cn
个人博文&#xff1a;《SpringBoot 手册》&#xff1a;国际化组件 MessageSource
CSDN 主页&#xff1a;https://kanshan.blog.csdn.net/
CSDN 博文&#xff1a;《SpringBoot 手册》&#xff1a;国际化组件 MessageSource

&#x1f447;&#x1f3fb;欢迎关注我的公众号「看山的小屋」&#xff0c;领取精选资料&#x1f447;&#x1f3fb;

推荐阅读
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • 本文讨论了在Spring 3.1中,数据源未能自动连接到@Configuration类的错误原因,并提供了解决方法。作者发现了错误的原因,并在代码中手动定义了PersistenceAnnotationBeanPostProcessor。作者删除了该定义后,问题得到解决。此外,作者还指出了默认的PersistenceAnnotationBeanPostProcessor的注册方式,并提供了自定义该bean定义的方法。 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • springmvc学习笔记(十):控制器业务方法中通过注解实现封装Javabean接收表单提交的数据
    本文介绍了在springmvc学习笔记系列的第十篇中,控制器的业务方法中如何通过注解实现封装Javabean来接收表单提交的数据。同时还讨论了当有多个注册表单且字段完全相同时,如何将其交给同一个控制器处理。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
author-avatar
suny
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有