写点什么

Spring Boot 两行代码轻松实现国际化

发布于: 2021 年 04 月 20 日
Spring Boot 两行代码轻松实现国际化

i18n 国际化

在开发中,国际化(Internationalization),也叫本地化,指的是一个网站(或应用)可以支持多种不同的语言,即可以根据用户所在的语言类型和国家/地区,显示不同的文字。能够让不同国家,不同语种的用户方便使用,提高用户体验性。

实现国际化,比较简单的实现方案就是根据不同的国家和语言开发不同的程序,分别用相应的语言文字显示,例如 Oracle 英文官网地址:https://www.oracle.com/index.html,中文官网地址:https://www.oracle.com/cn/index.html

一般比较大型的公司会使用这种根据不同的国家和语言开发不同的程序的形式实现国家化,其一人家公司有资源投入开发,其二可以根据不同国家,不同语种用户习惯开发更加符合当地人的布局样式,交互等。

还有另外一种国家化实现方案,就是开发一套程序,可以根据用户所在区域显示不同的语言文字,但是网站/应用的布局样式等不会发生很大变化。这个方案也是我们要将的 i18n 国际化实现,i18n 其实就是英文单词 Internationalization(国际化)的缩写,i 和 n 代表单词首尾字母,18 代表中间的 18 个字母。

i18n 实现

在 Java 中,通过 java.util.Locale 类表示本地化对象,它通过语言类型和国家/地区等元素来确定创建一个本地化对象 。Locale 对象表示具体的地理,时区,语言,政治等。

我们可以通过以下方法,获取本地系统的语言,国家等信息;以及获取代表指定地区的语言,国家信息 Local 对象。当然你也可以调用 Locale.getAvailableLocales() 方法查看所有可用的 Local 对象。

package com.nobody;
import java.util.Locale;
/** * @Description * @Author Mr.nobody * @Date 2021/4/15 * @Version 1.0 */public class LocalTest {    public static void main(String[] args) {        Locale defaultLocale = Locale.getDefault();        Locale chinaLocale = Locale.CHINA;        Locale usLocale = Locale.US;        Locale usLocale1 = new Locale("en", "US");        System.out.println(defaultLocale);        System.out.println(defaultLocale.getLanguage());        System.out.println(defaultLocale.getCountry());        System.out.println(chinaLocale);        System.out.println(usLocale);        System.out.println(usLocale1);    }}
// 输出结果zh_CNzhCNzh_CNen_USen_US
复制代码


我们一般会将不同的语言的属性值存放在不同的配置文件中,ResourceBundle 类可以根据指定的 baseName 和 Local 对象,就可以找到相应的配置文件,从而读取到相应的语言文字,从而构建出 ResourceBundle 对象,然后我们可以通过 ResourceBundle.getString(key)就可以取得 key 在不同地域的语言文字了。

Properties 配置文件命名规则:baseName_local.properties

假如 baseName 为 i18n,则相应的配置文件应该命名为如下:

  • 中文的配置文件:i18n_zh_CN.properties

  • 英文的配置文件:i18n_en_US.properties



然后在两个配置文件中,存放着键值对,对应不同的语言文字

# 在i18n_zh_CN.properties文件中userName=陈皮
# 在i18n_en_US.properties文件中userName=Peel
复制代码


我们通过如下方式,就可以获取相应语言环境下的信息了,如下:

Locale chinaLocale = Locale.CHINA;ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n", chinaLocale);String userName = resourceBundle.getString("userName");System.out.println(userName);
Locale usLocale = Locale.US;resourceBundle = ResourceBundle.getBundle("i18n", usLocale);userName = resourceBundle.getString("userName");System.out.println(userName);
// 输出结果陈皮Peel
复制代码


对于不同地域语言环境的用户,我们是如何处理国际化呢?其实原理很简单,假设客户端发送一个请求到服务端,在请求头中设置了键值对,“Accept-Language”:“zh-CN”,根据这个信息,可以构建出一个代表这个区域的本地化对象 Locale,根据配置文件的 baseName 和 Locale 对象就可以知道读取哪个配置文件的属性,将要显示的文字格式化处理,最终返回给客户端进行显示。

Springboot 集成 i18n

在 Springboot 中,我们会使用到一个MessageSource接口,用于访问国际化信息,此接口定义了几个重载的方法。code 即国际化资源的属性名(键);args 即传递给格式化字符串中占位符的运行时参数值;local 即本地化对象;resolvable 封装了国际化资源属性名,参数,默认信息等。

  • String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale)

  • String getMessage(String code, @Nullable Object[] args, Locale locale)

  • String getMessage(MessageSourceResolvable resolvable, Locale locale)

Springboot 提供了国际化信息自动配置类 MessageSourceAutoConfiguration,它可以生成 MessageSource 接口的实现类 ResourceBundleMessageSource,注入到 Spring 容器中。MessageSource 配置生效依靠 ResourceBundleCondition 条件,从环境变量中读取 spring.messages.basename 的值(默认值 messages),这个值就是 MessageSource 对应的资源文件名称,资源文件扩展名是.properties,然后通过 PathMatchingResourcePatternResolver 从classpath*:目录下读取对应的资源文件,如果能正常读取到资源文件,则加载配置类。源码如下:

package org.springframework.boot.autoconfigure.context;
@Configuration@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Conditional(ResourceBundleCondition.class)@EnableConfigurationPropertiespublic class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = {};
    // 我们可以在application.properties文件中修改spring.messages前缀的默认值,比如修改basename的值    @Bean    @ConfigurationProperties(prefix = "spring.messages")    public MessageSourceProperties messageSourceProperties() {        return new MessageSourceProperties();    }
    // 生成ResourceBundleMessageSource实例,注入容器中    @Bean    public MessageSource messageSource(MessageSourceProperties properties) {        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();        if (StringUtils.hasText(properties.getBasename())) {            messageSource.setBasenames(StringUtils                                       .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));        }        if (properties.getEncoding() != null) {            messageSource.setDefaultEncoding(properties.getEncoding().name());        }        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());        Duration cacheDuration = properties.getCacheDuration();        if (cacheDuration != null) {            messageSource.setCacheMillis(cacheDuration.toMillis());        }        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());        return messageSource;    }
    protected static class ResourceBundleCondition extends SpringBootCondition {
        private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
        @Override        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");            ConditionOutcome outcome = cache.get(basename);            if (outcome == null) {                outcome = getMatchOutcomeForBasename(context, basename);                cache.put(basename, outcome);            }            return outcome;        }
        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {            ConditionMessage.Builder message = 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 " + basename).atAll());        }
        // 读取classpath*:路径下的配置文件        private Resource[] getResources(ClassLoader classLoader, String name) {            String target = name.replace('.', '/');            try {                return new PathMatchingResourcePatternResolver(classLoader)                    .getResources("classpath*:" + target + ".properties");            }            catch (Exception ex) {                return NO_RESOURCES;            }        }
    }
}
复制代码


以下这个类是 Spring 国际化处理的属性配置类,我们可以在 application.properties 文件中自定义修改这些默认值,例如:spring.messages.basename=i18n

package org.springframework.boot.autoconfigure.context;
/** * Configuration properties for Message Source. * * @author Stephane Nicoll * @author Kedar Joshi * @since 2.0.0 */public class MessageSourceProperties {
    /**  * Comma-separated list of basenames (essentially a fully-qualified classpath  * location), each following the ResourceBundle convention with relaxed support for  * slash based locations. If it doesn't contain a package qualifier (such as  * "org.mypackage"), it will be resolved from the classpath root.  */    private String basename = "messages";
    /**  * Message bundles encoding.  */    private Charset encoding = StandardCharsets.UTF_8;
    /**  * Loaded resource bundle files cache duration. When not set, bundles are cached  * forever. If a duration suffix is not specified, seconds will be used.  */    @DurationUnit(ChronoUnit.SECONDS)    private Duration cacheDuration;
    /**  * Whether to fall back to the system Locale if no files for a specific Locale have  * been found. if this is turned off, the only fallback will be the default file (e.g.  * "messages.properties" for basename "messages").  */    private boolean fallbackToSystemLocale = true;
    /**  * Whether to always apply the MessageFormat rules, parsing even messages without  * arguments.  */    private boolean alwaysUseMessageFormat = false;
    /**  * Whether to use the message code as the default message instead of throwing a  * "NoSuchMessageException". Recommended during development only.  */    private boolean useCodeAsDefaultMessage = false;
    // 省略get/set}
复制代码


我们在类路径下创建好国际化配置文件之后,就可以注入 MessageSource 实例,进行国际化处理了:

i18n.properties 文件是默认文件,当找不到语言的配置的时候,使用该文件进行展示。


@Autowiredprivate MessageSource messageSource;
@GetMapping("test")public GeneralResult<String> test() {    // 获取客户端的语言环境Locale对象,即取的请求头Accept-Language键的值来判断,我们也可以自定义请求头键,来获取语言标识    Locale locale = LocaleContextHolder.getLocale();    String userName = messageSource.getMessage("userName", null, locale);    System.out.println(userName);    return GeneralResult.genSuccessResult(userName);}
复制代码


上面我们是利用 Spirng 自带的 LocaleContextHolder 来获取本地对象 Locale,它是取的请求头 Accept-Language 键的语言值来判断生成相应 Locale 对象。我们也可以根据其他方式,例如请求头中自定义键的值,来生成 Locale 对象,然后再通过 messageSource.getMessage()方法来实现最终的国家化。


推荐阅读

为什么阿里巴巴的程序员成长速度这么快

进大厂也就这回事,工作2到3年后进大厂操作指南

阿里架构师【柏羲】带你揭秘架构项目实战与源码解读:微博+B站架构设计、JUC核心、Mybatis源码

看完三件事

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 Java 斗帝 』,不定期分享原创知识。

同时可以期待后续文章 ing🚀

用户头像

还未添加个人签名 2020.09.07 加入

还未添加个人简介

评论

发布
暂无评论
Spring Boot 两行代码轻松实现国际化