原创

Spring系列第26篇:国际化详解

上次去一个电商公司面试:

面试官:Spring中国际化这块的东西用过么?可以介绍一下么?

我:spring中对国际化支持挺好的,比较简单,只需要按照语言配置几个properties文件,然后主要注册一个国际化的相关的bean,同时需指定一下配置文件的位置,基本上就可以了

面试官:那如果配置文件内容有变化?你们怎么解决的?

我:这块啊,spring国际化这块有个实现类,可以检测到配置文件的变化,就可以解决你这个问题

面试官:那我们是否可以将这些国际化的配置丢到db中去管理呢?

我:这个地方我没有搞过,基本上我们这边都是将国际化的配置文件放在项目中的properties文件中;不过以我对spring的理解,spring扩展方面是非常优秀的,应该是可以这么做的,自己去实现一下spring国际化相关接口就可以了。

面试官:工资期望多少?

我:2万

面试官:恭喜你,下周来上班!

为了方便大家,准备把这块知识细化一下,方便大家面试及使用。

本次问题

  1. Spring中国际化怎么用?
  2. 国际化如何处理资源文件变化的问题?
  3. 国际化资源配置放在db中如何实现?

先说一下什么是国际化

简单理解,就是对于不同的语言,做出不同的响应。

比如页面中有个填写用户信息的表单,有个姓名的输入框

浏览器中可以选择语言

选中文的时候会显示:

姓名:一个输入框

选英文的时候会显示:

Full name:一个输入框

国际化就是做这个事情的,根据不同的语言显示不同的信息。

所以需要支持国际化,得先知道选择的是哪种地区的哪种语言,java中使用java.util.Locale来表示地区语言这个对象,内部包含了国家和语言的信息。

Locale中有个比较常用的构造方法

public Locale(String language, String country) {
    this(language, country, "");
}

2个参数:

language:语言

country:国家

语言和国家这两个参数的值不是乱写的,国际上有统一的标准:

比如language的值:zh表示中文,en表示英语,而中文可能很多地区在用,比如大陆地区可以用:CN,新加坡用:SG;英语也是有很多国家用的,GB表示英国,CA表示加拿大

国家语言简写格式:language-country,如:zh-CN(中文【中国】),zh-SG(中文【新加坡】),en-GB(英语【英国】),

en-CA(英语【加拿大】)。

还有很多,这里就不细说了,国家语言编码给大家提供一个表格:http://www.itsoku.com/article/282

Locale类中已经创建好了很多常用的Locale对象,直接可以拿过来用,随便列几个看一下:

static public final Locale SIMPLIFIED_CHINESE = createConstant("zh", "CN"); //zh_CN
static public final Locale UK = createConstant("en", "GB"); //en_GB
static public final Locale US = createConstant("en", "US"); //en_US
static public final Locale CANADA = createConstant("en", "CA"); //en_CA

再回头看前面的问题:页面中显示姓名对应的标签,需要我们根据一个key及Locale信息来获取对应的国际化信息,spring中提供了这部分的实现,下面我们来看详情。

Spring中国际化怎么用?

MessageSource接口

spring中国际化是通过MessageSource这个接口来支持的

org.springframework.context.MessageSource

内部有3个常用的方法用来获取国际化信息,来看一下

public interface MessageSource {

    /**
     * 获取国际化信息
     * @param code 表示国际化资源中的属性名;
     * @param args用于传递格式化串占位符所用的运行期参数;
     * @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;
     * @param locale 表示本地化对象
     */
    @Nullable
    String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

    /**
     * 与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常
     */
    String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

    /**
     * @param MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个方法相同
     */
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}

常见3个实现类

ResourceBundleMessageSource

这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源

ReloadableResourceBundleMessageSource

这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息

StaticMessageSource

它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。

Spring中使用国际化的3个步骤

通常我们使用spring的时候,都会使用带有ApplicationContext字样的spring容器,这些容器一般是继承了AbstractApplicationContext接口,而这个接口实现了上面说的国际化接口MessageSource,所以通常我们用到的ApplicationContext类型的容器都自带了国际化的功能。

通常我们在ApplicationContext类型的容器中使用国际化3个步骤

步骤一:创建国际化文件

步骤二:向容器中注册一个MessageSource类型的bean,bean名称必须为:messageSource

步骤三:调用AbstractApplicationContext中的getMessage来获取国际化信息,其内部将交给第二步中注册的messageSource名称的bean进行处理

来个案例感受一下

创建国际化文件

国际化文件命名格式:名称_语言_地区.properties

我们来3个文件,文件都放在下面这个目录中

com/javacode2018/lesson002/demo19/
message.properties
name=您的姓名
personal_introduction=默认个人介绍:{0},{1}

这个文件名称没有指定Local信息,当系统找不到的时候会使用这个默认的

message_cn_ZH.properties:中文【中国】
name=姓名
personal_introduction=个人介绍:{0},{1},{0}
message_en_GB.properties:英文【英国】
name=Full name
personal_introduction=personal_introduction:{0},{1},{0}

spring中注册国际化的bean

注意必须是MessageSource类型的,bean名称必须为messageSource,此处我们就使用ResourceBundleMessageSource这个类

package com.javacode2018.lesson002.test19.demo1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;

@Configuration
public class MainConfig1 {
    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource result = new ResourceBundleMessageSource();
        //可以指定国际化化配置文件的位置,格式:路径/文件名称,注意不包含【语言_国家.properties】含这部分
        result.setBasenames("com/javacode2018/lesson002/demo19/message"); //@1
        return result;
    }
}

@1:这个地方的写法需要注意,可以指定国际化化配置文件的位置,格式:路径/文件名称,注意不包含【语言_国家.properties】含这部分

来个测试用例

package com.javacode2018.lesson002.test19;

import com.javacode2018.lesson002.test19.demo1.MainConfig1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Locale;

public class MessageSourceTest {

    @Test
    public void test1() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig1.class);
        context.refresh();
        //未指定Locale,此时系统会取默认的locale对象,本地默认的值中文【中国】,即:zh_CN
        System.out.println(context.getMessage("name", null, null));
        System.out.println(context.getMessage("name", null, Locale.CHINA)); //CHINA对应:zh_CN
        System.out.println(context.getMessage("name", null, Locale.UK)); //UK对应en_GB
    }
}

运行输出

您的姓名
您的姓名
Full name

第一行未指定Locale,此时系统会取默认的locale对象,本地默认的值中文【中国】,即:zh_CN,所以会获取到message_zh_CN.properties中的内容。

后面2行,都指定了Locale对象,找到对应的国际化文件,取值。

动态参数使用

注意配置文件中的personal_introduction,个人介绍,比较特别,包含了{0},{1},{0}这样一部分内容,这个就是动态参数,调用getMessage的时候,通过第二个参数传递过去,来看一下用法:

@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig1.class);
    context.refresh();
    //未指定Locale,此时系统会取默认的,本地电脑默认的值中文【中国】,即:zh_CN
    System.out.println(context.getMessage("personal_introduction", new String[]{"spring高手", "java高手"}, Locale.CHINA)); //CHINA对应:zh_CN
    System.out.println(context.getMessage("personal_introduction", new String[]{"spring", "java"}, Locale.UK)); //UK对应en_GB
}

运行输出

默认个人介绍:spring高手,java高手
personal_introduction:spring,java,spring

监控国际化文件的变化

ReloadableResourceBundleMessageSource这个类,功能和上面案例中的ResourceBundleMessageSource类似,不过多了个可以监控国际化资源文件变化的功能,有个方法用来设置缓存时间:

public void setCacheMillis(long cacheMillis)

-1:表示永远缓存

0:每次获取国际化信息的时候,都会重新读取国际化文件

大于0:上次读取配置文件的时间距离当前时间超过了这个时间,重新读取国际化文件

还有个按秒设置缓存时间的方法setCacheSeconds,和setCacheMillis类似

下面我们来案例

package com.javacode2018.lesson002.test19.demo2;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

@Configuration
public class MainConfig2 {
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource result = new ReloadableResourceBundleMessageSource();
        result.setBasenames("com/javacode2018/lesson002/demo19/message");
        //设置缓存时间1000毫秒
        result.setCacheMillis(1000);
        return result;
    }
}

message_zh_CN.properties中新增一行内容

address=上海

对应的测试用例

@Test
public void test3() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig2.class);
    context.refresh();
    //输出2次
    for (int i = 0; i < 2; i++) {
        System.out.println(context.getMessage("address", null, Locale.CHINA));
        TimeUnit.SECONDS.sleep(5);
    }
}

上面有个循环,当第一次输出之后,修改一下message_zh_CN.properties中的address为上海松江,最后运行结果如下:

上海
上海松江

使用注意:线上环境,缓存时间最好设置大一点,性能会好一些。

国际化信息存在db中

上面我们介绍了一个类:StaticMessageSource,这个类它允许通过编程的方式提供国际化信息,我们通过这个类来实现从db中获取国际化信息的功能。

这个类中有2个方法比较重要:

public void addMessage(String code, Locale locale, String msg);
public void addMessages(Map<String, String> messages, Locale locale);

通过这两个方法来添加国际化配置信息。

下面来看案例

自定义一个StaticMessageSource类

package com.javacode2018.lesson002.test19.demo3;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.StaticMessageSource;

import java.util.Locale;

public class MessageSourceFromDb extends StaticMessageSource implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        //此处我们在当前bean初始化之后,模拟从db中获取国际化信息,然后调用addMessage来配置国际化信息
        this.addMessage("desc", Locale.CHINA, "我是从db来的信息");
        this.addMessage("desc", Locale.UK, "MessageSource From Db");
    }
}

上面的类实现了spring的InitializingBean接口,重写了接口中干掉afterPropertiesSet方法,这个方法会在当前bean初始化之后调用,在这个方法中模拟从db中获取国际化信息,然后调用addMessage来配置国际化信息

来个spring配置类,将MessageSourceFromDb注册到spring容器

package com.javacode2018.lesson002.test19.demo3;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MainConfig3 {
    @Bean
    public MessageSource messageSource(){
        return new MessageSourceFromDb();
    }
}

上测试用例

@Test
public void test4() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig3.class);
    context.refresh();
    System.out.println(context.getMessage("desc", null, Locale.CHINA));
    System.out.println(context.getMessage("desc", null, Locale.UK));
}

运行输出

我是从db来的信息
MessageSource From Db

bean名称为什么必须是messageSource

上面我容器启动的时候会调用refresh方法,过程如下:

org.springframework.context.support.AbstractApplicationContext#refresh
内部会调用
org.springframework.context.support.AbstractApplicationContext#initMessageSource
这个方法用来初始化MessageSource,方法内部会查找当前容器中是否有messageSource名称的bean,如果有就将其作为处理国际化的对象
如果没有找到,此时会注册一个名称为messageSourceMessageSource

自定义bean中使用国际化

自定义的bean如果想使用国际化,比较简单,只需实现下面这个接口,spring容器会自动调用这个方法,将MessageSource注入,然后我们就可以使用MessageSource获取国际化信息了。

public interface MessageSourceAware extends Aware {
    void setMessageSource(MessageSource messageSource);
}

总结

本文介绍了国际化的使用,涉及到了java中的Locale类,这个类用来表示语言国家信息,获取国际化信息的时候需要携带这个参数,spring中通过MessageSource接口来支持国际化的功能,有3个常用的实现类需要了解,StaticMessageSource支持硬编码的方式配置国际化信息。

如果需要spring支撑国际化,需要注册一个bean名称为messageSource的MessageSource,这个一定要注意。

到此,上面面试的3个问题,大家都能轻松应对了。

案例源码

https://gitee.com/javacode2018/spring-series

路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。

Spring系列

  1. Spring系列第1篇:为何要学spring?
  2. Spring系列第2篇:控制反转(IoC)与依赖注入(DI)
  3. Spring系列第3篇:Spring容器基本使用及原理
  4. Spring系列第4篇:xml中bean定义详解(-)
  5. Spring系列第5篇:创建bean实例这些方式你们都知道?
  6. Spring系列第6篇:玩转bean scope,避免跳坑里!
  7. Spring系列第7篇:依赖注入之手动注入
  8. Spring系列第8篇:自动注入(autowire)详解,高手在于坚持
  9. Spring系列第9篇:depend-on到底是干什么的?
  10. Spring系列第10篇:primary可以解决什么问题?
  11. Spring系列第11篇:bean中的autowire-candidate又是干什么的?
  12. Spring系列第12篇:lazy-init:bean延迟初始化
  13. Spring系列第13篇:使用继承简化bean配置(abstract & parent)
  14. Spring系列第14篇:lookup-method和replaced-method比较陌生,怎么玩的?
  15. Spring系列第15篇:代理详解(Java动态代理&cglib代理)?
  16. Spring系列第16篇:深入理解java注解及spring对注解的增强(预备知识)
  17. Spring系列第17篇:@Configration和@Bean注解详解(bean批量注册)
  18. Spring系列第18篇:@ComponentScan、@ComponentScans详解(bean批量注册)
  19. Spring系列第18篇:@import详解(bean批量注册)
  20. Spring系列第20篇:@Conditional通过条件来控制bean的注册
  21. Spring系列第21篇:注解实现依赖注入(@Autowired、@Resource、@Primary、@Qulifier)
  22. Spring系列第22篇:@Scope、@DependsOn、@ImportResource、@Lazy 详解
  23. Spring系列第23篇:Bean生命周期详解
  24. Spring系列第24篇:父子容器详解
  25. Spring系列第25篇:@Value【用法、数据来源、动态刷新】

更多好文章

  1. Java高并发系列(共34篇)
  2. MySql高手系列(共27篇)
  3. Maven高手系列(共10篇)
  4. Mybatis系列(共12篇)
  5. 聊聊db和缓存一致性常见的实现方式
  6. 接口幂等性这么重要,它是什么?怎么实现?
  7. 泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!

想了解更多精彩内容请关注我的公众号:路人甲Java

正文到此结束
本文目录