本文对应源码地址:
https://github.com/nieandsun/NRSC-STUDY/tree/master/i18n-study

1 引子

1.1 国际化简单概述

作为一个服务端开发人员,这里我想先站在自己的角度对国际化(internationalization,因在i和n之间共有18个字母,所以国际化也称为i18n)所要做的事做一个简单的概述:

国际化在实际项目中所要承担的职责是按照客户指定的语言让服务端返回相应语言的内容。


1.2 spring/springboot工程中国际化玩法概述

在spring/springboot的世界里,国际化的玩法是基于如下接口的:
org.springframework.context.MessageSource
在该接口里主要定义了如下三个方法:

public interface MessageSource {
    //如果在国际化配置文件中找不到var1对应的message,可以给一个默认值var3
    @Nullable
    String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);
	//如果在国际化配置文件中找不到var1对应的message,抛出异常
    String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;
	//这个方法暂时没有做太多研究,所以本篇文章也不会涉及
    String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException;
}

该接口比较重要的三个实现类如下:

  • ResourceBundleMessageSource
  • ReloadableResourceBundleMessageSource
  • StaticMessageSource

它们与MessageSource间的继承关系如下:
在这里插入图片描述
接下来将对这个三个类的玩法进行具体地介绍。

2 ResourceBundleMessageSource的玩法(默认)

首先来看一下在springboot项目中国际化最基础的玩法:

(1)搭建一个springboot项目(至少添加web依赖)。

(2)创建国际化配置文件,如下图所示:
在这里插入图片描述
messages.properties(默认配置)

 user.name=yoyo

messages_en_US.properties

user.name=yoyo-EN
user.name1=nrsc
user.name2=nrsc{0}-{1}

messages_zh_CN.properties

user.name1=章尔
user.name2=章尔{0}-{1}

(3)创建controller测试类

@RestController
public class I18nDemoController {
    @Autowired
    private MessageSource messageSource;

    @GetMapping("/hello")
    public String hello() {
        String defaultM = messageSource
                .getMessage("user.name", null, LocaleContextHolder.getLocale());
        String message1 = messageSource
                .getMessage("user.name1", null, LocaleContextHolder.getLocale());
        String message2 = messageSource
                .getMessage("user.name2", new String[]{"WW", "MM"}, LocaleContextHolder.getLocale());
        String message3 = messageSource
                .getMessage("user.nameXX", null, "defaultName", LocaleContextHolder.getLocale());

        return defaultM + "<->" + message1 + "--" + message2 + "##" + message3;
    }
}

tips:

① 直接可以从spring容器里通过@Autowired拿到MessageSource的原因是如果没有配置该类型的bean时,spring容器会默认初始化一个该类型的bean-ResourceBundleMessageSource 放到spring容器里。答案在如下源码文件中:
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration

②LocaleContextHolder.getLocale()可以拿到本次请求header里的Accept-Language对应的语言环境 。

(4)简单测试一种场景,结果如下(其他场景留给读者):
在这里插入图片描述

3 ReloadableResourceBundleMessageSource的玩法

ReloadableResourceBundleMessageSource 相比于ResourceBundleMessageSource 而言最重要的区别在于

  • 前者可以读取.properties和.xml结尾的国际化映射文件,后者只可以读取.properties结尾的文件
  • 前者可以指定映射文件在内存中缓存的时间,后者不可以

ReloadableResourceBundleMessageSource的具体玩法如下:

(1)创建配置化文件,如下图:
在这里插入图片描述

(2)创建ReloadableResourceBundleMessageSource,将其注入到spring容器,代码如下:

@Bean("reloadableResourceBundleMessageSource")
public MessageSource initReloadableResourceBundleMessageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    //指定读取国际化配置文件的basename
    messageSource.setBasename(ResourceUtils.CLASSPATH_URL_PREFIX + "i18n/messages");
    //指定编码
    messageSource.setDefaultEncoding("UTF-8");
    //指定缓存时间
    messageSource.setCacheSeconds(60);
    return messageSource;
}

(3)指定注入的MessageSource为ReloadableResourceBundleMessageSource

@Autowired
@Qualifier("reloadableResourceBundleMessageSource")
private MessageSource messageSource;

(4)测试留给读者

4 StaticMessageSource的玩法

ReloadableResourceBundleMessageSourceResourceBundleMessageSource都是基于本地文件的,而StaticMessageSource就比较简单了,它的基本玩法如下:

(1)创建StaticMessageSource,同时指定国际化映射内容,然后将其放入spring容器,代码如下:

/****
 * code和msg可以来自于数据库,或者其他任何文件系统
 * @return
 */
@Bean("staticMessageSource")
public MessageSource initStaticMessageSource() {
    StaticMessageSource messageSource = new StaticMessageSource();

    messageSource.addMessage("user.name", Locale.US, "yoyo-EN");
    messageSource.addMessage("user.name1", Locale.US, "nrsc");
    messageSource.addMessage("user.name2", Locale.US, "nrsc{0}-{1}");

    messageSource.addMessage("user.name", Locale.CHINA, "章尔");
    messageSource.addMessage("user.name1", Locale.CHINA, "章尔1");
    messageSource.addMessage("user.name2", Locale.CHINA, "章尔{0}-{1}");
    return messageSource;
}

(2)指定注入的MessageSource为StaticMessageSource

@Autowired
//@Qualifier("reloadableResourceBundleMessageSource")
@Qualifier("staticMessageSource")
private MessageSource messageSource;

(3)测试留给读者

5 DIY

5.1 WHY DIY?

首先假设你的项目里国际化实现方案上有如下两个技术需求:

  • 需要进行翻译的内容较多,要进行结构化的存储与管理(比如存放到mysql数据库)
  • 希望可以借助redis进行缓存

这时你会发现, spring/springboot提供的上面三种玩法貌似都不得行,这时我们就要考虑DIY了。

5.2 从StaticMessageSource源码找寻DIY灵感

依我的经验来看,要进行DIY最好的途径是站在源码的基础上进行模仿和改造。而上面介绍的三种玩法中,StaticMessageSource对应的玩法应该是最简单,也是最好入手的,这里简单撸一下它的源码:

public class StaticMessageSource extends AbstractMessageSource {

	/** Map from 'code + locale' keys to message Strings. */
	//保存key和message的map【key的格式举例:user.name1_zh_CN】
	private final Map<String, String> messages = new HashMap<>();
	//保存key和MessageFormat的map
	//当message里有占位符时(比如我上面例子里的nrsc{0}-{1}),会用到这个map
	private final Map<String, MessageFormat> cachedMessageFormats = new HashMap<>();

	//给定code和语言环境(即locale)从messages这个map里取出对应语言的message
	@Override
	protected String resolveCodeWithoutArguments(String code, Locale locale) {
		return this.messages.get(code + '_' + locale.toString());
	}
	//给定code和语言环境(即locale)从cachedMessageFormats这个map里
	//取出对应语言的MessageFormat,父类会联合占位符等信息,解析得到具体的message
	@Override
	@Nullable
	protected MessageFormat resolveCode(String code, Locale locale) {
		String key = code + '_' + locale.toString();
		String msg = this.messages.get(key);
		if (msg == null) {
			return null;
		}
		//这里采用了懒加载的方式,一开始的时候cachedMessageFormats这个map是空的
		//当被调用到后会根据msg、local生成MessageFormat
		//然后将生成的MessageFormat放到cachedMessageFormats这个map里
		synchronized (this.cachedMessageFormats) {
			MessageFormat messageFormat = this.cachedMessageFormats.get(key);
			if (messageFormat == null) {
				messageFormat = createMessageFormat(msg, locale);
				this.cachedMessageFormats.put(key, messageFormat);
			}
			return messageFormat;
		}
	}
	
	/**
	 * Associate the given message with the given code.
	 * @param code the lookup code
	 * @param locale the locale that the message should be found within
	 * @param msg the message associated with this lookup code
	 */
	 //添加Message
	public void addMessage(String code, Locale locale, String msg) {
		Assert.notNull(code, "Code must not be null");
		Assert.notNull(locale, "Locale must not be null");
		Assert.notNull(msg, "Message must not be null");
		this.messages.put(code + '_' + locale.toString(), msg);
		if (logger.isDebugEnabled()) {
			logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]");
		}
	}

	/**
	 * Associate the given message values with the given keys as codes.
	 * @param messages the messages to register, with messages codes
	 * as keys and message texts as values
	 * @param locale the locale that the messages should be found within
	 */
	 //批量添加Message
	public void addMessages(Map<String, String> messages, Locale locale) {
		Assert.notNull(messages, "Messages Map must not be null");
		messages.forEach((code, msg) -> addMessage(code, locale, msg));
	}


	@Override
	public String toString() {
		return getClass().getName() + ": " + this.messages;
	}
}

从上面的源码来看,其实非常简单。

5.3 DO DIY – 借助redis进行缓存

这里给出一个简单的借助redis进行缓存的DIY方案
(1)向redis里存储数据

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Test
public void initData() {
    List<MessageInfo> messageInfos = Arrays.asList(
            new MessageInfo("user.name", Locale.US.toString(), "yoyo-EN"),
            new MessageInfo("user.name1", Locale.US.toString(), "nrsc"),
            new MessageInfo("user.name2", Locale.US.toString(), "nrsc{0}-{1}"),
            new MessageInfo("user.name", Locale.CHINA.toString(), "章尔"),
            new MessageInfo("user.name1", Locale.CHINA.toString(), "章尔1"),
            new MessageInfo("user.name2", Locale.CHINA.toString(), "章尔{0}-{1}")
    );
    redisTemplate.opsForValue().set("userInfo", messageInfos);
}

(2)仿照StaticMessageSource自定义MessageSource

@Component("myMessageSource")
public class MyMessageSource extends AbstractMessageSource {

    private final Map<String, MessageFormat> cachedMessageFormats = new HashMap<>();

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    protected String resolveCodeWithoutArguments(String code, Locale locale) {
        Map<String, String> map = getMessagesMap();
        return map.get(code + '_' + locale.toString());
    }

    private Map<String, String> getMessagesMap() {
        Object userInfoList = redisTemplate.opsForValue().get("userInfo");
        List<MessageInfo> messageInfoList = (List<MessageInfo>) userInfoList;
        Map<String, String> map = new HashMap<>();
        for (MessageInfo messageInfo : messageInfoList) {
            String key = messageInfo.getCode() + '_' + messageInfo.getLocale();
            map.computeIfAbsent(key, k -> messageInfo.getMessage());
        }
        return map;
    }

    @Override
    @Nullable
    protected MessageFormat resolveCode(String code, Locale locale) {
        String key = code + '_' + locale.toString();
        String msg = getMessagesMap().get(key);
        if (msg == null) {
            return null;
        }
        synchronized (this.cachedMessageFormats) {
            MessageFormat messageFormat = this.cachedMessageFormats.get(key);
            if (messageFormat == null) {
                messageFormat = createMessageFormat(msg, locale);
                this.cachedMessageFormats.put(key, messageFormat);
            }
            return messageFormat;
        }
    }
}

(3)指定注入的MessageSource为我自定义的MyMessageSource

@Autowired
//@Qualifier("reloadableResourceBundleMessageSource")
//@Qualifier("staticMessageSource")
@Qualifier("myMessageSource")
private MessageSource messageSource;

(4)测试留给读者

本文由mdnice多平台发布

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐