使用总结

在resources目录下创建 messages.properties、messages_en_US.properties、messages_zh_CN.properties 国际化文件,在代码中注入MessageSource,通过MessageSource获取当前语言对应国际化文件的文本。

一.项目结构

springboot 给自己发送消息 解耦 springboot messagesource_语言环境

 二.快速使用

1.pom.xml中引入 spring-boot-starter 基础包即可。

<!-- springboot 基础包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- springboot web包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

spring-boot-starter 中包含了 spring-boot-autoconfigure,spring-boot-autoconfigure 包下有一个MessageSourceAutoConfiguration 配置类,SpringBoot会通过加载MessageSourceAutoConfiguration 自动注入一个 MessageSource 实例。

2.在resources目录下创建国际化配置文件 messages.properties、messages_en_US.properties、messages_zh_CN.properties。

springboot 给自己发送消息 解耦 springboot messagesource_spring_02

我们的 messages 文件是直接创建在 resources 目录下的,IDEA 在展示的时候,会添加一层 Resource Bundle,这个大家不用管,千万别手动去创建这个目录。

messages.properties:

#默认翻译文件,内容可以留空,但文件必须存在。

messages_en_US.properties:

user.name=zhangsan

messages_zh_CN.properties:

user.name=张三

3.内容国际化

@Slf4j
@RestController
@RequestMapping("/hello")
public class HelloController {

    @Resource
    MessageSource messageSource;

    @PostMapping("/getName")
    public BaseResponse<String> getName() {
        String message = messageSource.getMessage("user.name", null, LocaleContextHolder.getLocale());
        return BaseResponse.success(message);
    }
}
@Data
public class BaseResponse<T> implements Serializable {
    private static final long serialVersionUID = -619135033544136919L;
    private Integer code;
    private String msg;
    private T data;

    public static <T> BaseResponse<T> success(T data) {
        BaseResponse<T> response = new BaseResponse<T>();
        response.setCode(200);
        response.setMsg("success");
        response.setData(data);
        return response;
    }
}

代码中注入 MessageSource ,即可通过 MessageSource 根据key获取当前语言对应resources目录下国际化文件中的文本内容。

4.接口请求

启动项目,Postman工具请求接口:

springboot 给自己发送消息 解耦 springboot messagesource_国际化_03

springboot 给自己发送消息 解耦 springboot messagesource_国际化_04

至此,spring boot 中国际化使用已完成。

0.内容补充

Locale:语言环境。

LocaleResolver:语言解析器,默认的实现类是 AcceptHeaderLocaleResolver。WebMvcAutoConfiguration springboot中对应的自动注入配置类。

MessageSource:信息源,默认实现类 ResourceBundleMessageSource。MessageSourceAutoConfiguration springboot中对应的自动注入配置类。

可以看一下AcceptHeaderLocaleResolver的解析代码:

public Locale resolveLocale(HttpServletRequest request) {
        Locale defaultLocale = this.getDefaultLocale();
        if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
            //代码中设置了Locale 且 请求头中‘Accept-Language’为空,返回设置的Locale
            return defaultLocale;
        } else {
            //否则,从HttpServletRequest中取当前Locale
            Locale requestLocale = request.getLocale();
            List<Locale> supportedLocales = this.getSupportedLocales();
            if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
                Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
                if (supportedLocale != null) {
                    return supportedLocale;
                } else {
                    return defaultLocale != null ? defaultLocale : requestLocale;
                }
            } else {
                return requestLocale;
            }
        }
    }

AcceptHeaderLocaleResolver 主要是使用HttpServletRequest 对象根据请求头中的“Accept-Language”获取Locale语言环境。然后,如果“Accept-Language”的值为非法值,会去默认的messages.properties 文件中取文本。

除了默认的AcceptHeaderLocaleResolver,spring的 i18n 包中还提供了CookieLocaleResolver、SessionLocaleResolver、FixedLocaleResolver等实现类满足其它需求,当然,用户也可以自定义实现类自定义解析方式。

SessionLocaleResolver 的使用

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    /**
     * 语言环境切换拦截器
     *
     * @return
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 设置拦截请求url上参数的key,不设置,默认值"locale"
        interceptor.setParamName("lang");
        return interceptor;
    }

    /**
     * 自定义语言解析器,覆盖默认的AcceptHeaderLocaleResolver
     * tips:返回类型必须声明为LocaleResolver 接口类型,不然不会替换掉默认的AcceptHeaderLocaleResolver
     *
     * @return
     */
    @Bean("localeResolver")
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        // 设置默认语言环境"中文"
        localeResolver.setDefaultLocale(Locale.CHINA);
        return localeResolver;
    }
}

提供了一个 SessionLocaleResolver 实例,SessionLocaleResolver 将客户端的 Locale 保存到 HttpSession 对象中,并且可以进行修改。只要 session 有效,浏览器就不必重复告诉服务端当前的语言环境。

另外还配置了一个拦截器监听语言环境的改变,这个拦截器会拦截请求url上 key 为 "lang" 的参数,这个参数指定当前的语言环境。语言环境的切换在请求url上指定“lang”参数即可,如下:

http://localhost:8081/hello/getName?lang=en-US
http://localhost:8081/hello/getName?lang=zh-CN

至于bean SessionLocaleResolver 的返回类型要声明为LocaleResolver 类型的原因(或者@Bean("localeResolver")显式声明bean名称),见下图: 

 CookieLocaleResolver 的使用

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    /**
     * 语言环境切换拦截器
     *
     * @return
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 设置拦截请求url上参数的key,不设置,默认值"locale"
        interceptor.setParamName("lang");
        return interceptor;
    }

    // /**
    //  * 自定义语言解析器,覆盖默认的AcceptHeaderLocaleResolver
    //  * tips:返回类型必须声明为LocaleResolver 接口类型,不然不会替换掉默认的AcceptHeaderLocaleResolver
    //  *
    //  * @return
    //  */
    // @Bean("localeResolver")
    // public LocaleResolver localeResolver() {
    //     SessionLocaleResolver localeResolver = new SessionLocaleResolver();
    //     // 设置默认语言环境"中文"
    //     localeResolver.setDefaultLocale(Locale.CHINA);
    //     return localeResolver;
    // }

    /**
     * @return
     */
    @Bean("localeResolver")
    public LocaleResolver localeResolver() {
        // CookieLocaleResolver优先级低于LocaleChangeInterceptor 拦截器的语言设置
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.CHINA);
        // cookie有效期1天,单位:秒,默认-1表示Cookie在浏览器关闭之后就失效
        localeResolver.setCookieMaxAge(60 * 60 * 24);
        // 设置cookie中语言环境的key,会根据这个key动态更新语言环境.
        localeResolver.setCookieName("lang");
        return localeResolver;
    }
}

SessionLocaleResolver vs CookieLocaleResolver

SessionLocaleResolver 是根据Session中特定的属性值确定Locale。SessionLocaleResolver会查找Session中属性名为 org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE 的值,并将其转成Locale对象。

当session不存在,它还是根据accept-language HTTP请求头确定语言环境信息。

CookieLocaleResolver 是根据代码中指定的Cookie确定Locale。即上文的“localeResolver.setCookieName("lang");”。

当cookie不存在,它还是根据accept-language HTTP请求头确定环境信息。

SessionLocaleResolver和CookieLocaleResolver的区别是:前者一般要求用户登录后生成相应的用户会话才有效,而后者只要浏览器有Cookie存在即可。

 工具类抽取

/**
 * 国际化工具类
 *
 * @author yangzihe
 */
@Component
public class MessageUtils {

    public static MessageSource messageSource;

    public MessageUtils(MessageSource messageSource) {
        // spring的bean注入
        MessageUtils.messageSource = messageSource;
    }

    /**
     * 获取国际化翻译值
     */
    public static String get(String msgKey) {
        return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
    }

    /**
     * 获取中文翻译值
     */
    public static String getWithChina(String msgKey) {
        return messageSource.getMessage(msgKey, null, Locale.CHINA);
    }

    /**
     * 获取国际化翻译值(包括占位符)
     */
    public static String get(String msgKey, Object[] objects) {
        return messageSource.getMessage(msgKey, objects, LocaleContextHolder.getLocale());
    }
}

国际化中占位符{0}的使用

messages_zh_CN.properties:

user.name=张三
user.age=用户当前年龄为:{0}

代码中使用:

@Slf4j
@RestController
@RequestMapping("/hello")
public class HelloController {
    @PostMapping("/getName")
    public BaseResponse<String> getName() {
        String message = MessageUtils.get("user.name");
        return BaseResponse.success(message);
    }

    @PostMapping("/getAge")
    public BaseResponse<String> getAge() {
        Object[] objects = new Object[]{18};
        String message = MessageUtils.get("user.age", objects);
        return BaseResponse.success(message);
    }
}

请求:

springboot 给自己发送消息 解耦 springboot messagesource_国际化_05

可看到文件中:"用户当前年龄为:{0}" 输出的文本为:"用户当前年龄为:18",占位符{0}被替换成了数字18。

语言环境代码中动态更新

当时的项目前后端有WebSocket通信的需求,socket通信使用的Locale无法从http的cookie或者session中取,需要独立传递。所以,前端在发起socket连接请求的时候传递lang参数,后端socket根据socket的lang参数设置当前语言环境。设置语言环境工具类如下:

/**
 * 语言环境工具类
 *
 * @author yangzihe
 * @date 2021/7/2
 */
public class LocaleUtils {

    private static final String ZH_CN = "zh-CN";
    private static final String EN_US = "en-US";
    /**
     * 自定义语言、国家:越南语
     */
    private static final String VN_VN = "vn-VN";

    /**
     * 设置语言环境
     *
     * @param lang 语言环境
     */
    public static void setLocale(String lang) {
        if (StringUtils.isBlank(lang)) {
            return;
        }
        Locale locale = null;
        if (ZH_CN.equalsIgnoreCase(lang)) {
            locale = Locale.CHINA;
        } else if (EN_US.equalsIgnoreCase(lang)) {
            locale = Locale.US;
        } else if (VN_VN.equalsIgnoreCase(lang)) {
            locale = new Locale("vn", "VN");
        } else {
            locale = Locale.CHINA;
        }
        LocaleContextHolder.setLocale(locale);
    }
}

自定义国际化文件位置

例如:我们想把国际化配置文件放在 resources/i18n 目录下:

springboot 给自己发送消息 解耦 springboot messagesource_spring_06

那么,就需要在 application.properties 中进行额外配置:

spring.messages.basename=i18n/messages

国际化文件命名规范 

基本格式:基名_语言_地区.properties
常见的资源文件命名规范
i18n_en.properties:所有英文语言的资源
i18n_en_US.properties:美国地区,英文语言的资源
i18n_zh.properties:所有中文语言的资源
i18n_zh_CN.properties:中国大陆的,中文语言的资源
基名.properties:默认资源文件,如果请求相应的资源文件不存在,将使用此资源文件

命名规范:

日本 : ja_JP
秘鲁 : es_PE
巴拿马 : es_PA
波斯尼亚和黑山共和国 : sr_BA
危地马拉 : es_GT
阿拉伯联合酋长国 : ar_AE
挪威 : no_NO
阿尔巴尼亚 : sq_AL
伊拉克 : ar_IQ
也门 : ar_YE
葡萄牙 : pt_PT
塞浦路斯 : el_CY
卡塔尔 : ar_QA
马其顿王国 : mk_MK
瑞士 : de_CH
美国 : en_US
芬兰 : fi_FI
马耳他 : en_MT
斯洛文尼亚 : sl_SI
斯洛伐克 : sk_SK
土耳其 : tr_TR
沙特阿拉伯 : ar_SA
英国 : en_GB
塞尔维亚及黑山 : sr_CS
新西兰 : en_NZ
挪威 : no_NO
立陶宛 : lt_LT
尼加拉瓜 : es_NI
爱尔兰 : ga_IE
比利时 : fr_BE
西班牙 : es_ES
黎巴嫩 : ar_LB
加拿大 : fr_CA
爱沙尼亚 : et_EE
科威特 : ar_KW
塞尔维亚 : sr_RS
美国 : es_US
墨西哥 : es_MX
苏丹 : ar_SD
印度尼西亚 : in_ID
乌拉圭 : es_UY
拉脱维亚 : lv_LV
巴西 : pt_BR
叙利亚 : ar_SY
多米尼加共和国 : es_DO
瑞士 : fr_CH
印度 : hi_IN
委内瑞拉 : es_VE
巴林 : ar_BH
菲律宾 : en_PH
突尼斯 : ar_TN
奥地利 : de_AT
荷兰 : nl_NL
厄瓜多尔 : es_EC
台湾地区 : zh_TW
约旦 : ar_JO
冰岛 : is_IS
哥伦比亚 : es_CO
哥斯达黎加 : es_CR
智利 : es_CL
埃及 : ar_EG
南非 : en_ZA
泰国 : th_TH
希腊 : el_GR
意大利 : it_IT
匈牙利 : hu_HU
爱尔兰 : en_IE
乌克兰 : uk_UA
波兰 : pl_PL
卢森堡 : fr_LU
比利时 : nl_BE
印度 : en_IN
西班牙 : ca_ES
摩洛哥 : ar_MA
玻利维亚 : es_BO
澳大利亚 : en_AU
新加坡 : zh_SG
萨尔瓦多 : es_SV
俄罗斯 : ru_RU
韩国 : ko_KR
阿尔及利亚 : ar_DZ
越南 : vi_VN
黑山 : sr_ME
利比亚 : ar_LY
中国 : zh_CN
台湾:zh_TW
香港 : zh_HK
白俄罗斯 : be_BY
以色列 : iw_IL
保加利亚 : bg_BG
马耳他 : mt_MT
巴拉圭 : es_PY
法国 : fr_FR
捷克共和国 : cs_CZ
瑞士 : it_CH
罗马尼亚 : ro_RO
波多黎哥 : es_PR
加拿大 : en_CA
德国 : de_DE
卢森堡 : de_LU
阿根廷 : es_AR
马来西亚 : ms_MY
克罗地亚 : hr_HR
新加坡 : en_SG
阿曼 : ar_OM
泰国 : th_TH
瑞典 : sv_SE
丹麦 : da_DK
洪都拉斯 : es_HN