一、前言
随着一个系统的规模做上去之后,国际化的问题就会逐渐暴露出来,简单来说,当你的系统面对的不再是本国的用户,而要面临海外用户时,系统必须要能适配国际化。系统国际化这个事情,互联网经历了多年的发展之后,尤其是移动端的适配方案已经很成熟,本文以微服务项目为例,具体来说,以springboot框架为例进行说明如何在微服务项目中运用国际化解决方案。
二、国际化概述
2.1 微服务中的国际化是什么
微服务中的国际化(Internationalization,简称I18n)是指设计和开发产品的过程,使得它们能够适应多种语言和文化环境,而不需要进行大量的代码更改。这通常涉及到创建一个基础版本的产品,然后通过配置和资源文件来添加对不同语言和地区的支持。当产品需要在新的地理区域或语言环境中使用时,只需要添加或更新相应的资源文件,而不需要修改产品本身的代码。
2.1.1 国际化概念
- 国际化(i18n):设计和开发软件应用,使其能够适应不同语言、文化和地区的需求,而无需进行重大修改。
- 本地化(l10n):将国际化的应用定制化,以适应特定地区的语言和文化。
2.1.2 为什么需要国际化
在微服务架构中,系统国际化尤为重要,原因如下:
- 支持多语言用户- 全球市场:微服务系统通常服务于全球用户,这些用户可能来自不同的国家和地区,使用不同的语言。通过国际化,可以确保所有用户都能看到他们习惯的语言界面。- 用户体验:提供本地化的内容可以显著提升用户体验,使用户感到更加亲切和舒适。
- 提高市场竞争力- 扩大市场:支持多种语言和文化可以使产品覆盖更广泛的市场,从而增加潜在用户基数。- 品牌信任:本地化的应用更容易赢得用户的信任,尤其是在非英语国家和地区。
- 法规约束- 法律要求:某些国家和地区有明确的法律法规要求,规定在特定市场销售的产品必须支持当地语言和文化。国际化可以帮助企业遵守这些法规,避免法律风险。
- 文化敏感性- 尊重多样性:不同文化对某些内容和表达方式有不同的敏感度。通过国际化,可以确保应用内容符合不同文化的要求,避免冒犯用户。- 节日和习俗:不同国家和地区有不同的节日和习俗,国际化可以帮助应用更好地融入当地文化,提供相关的功能和服务。
- 技术可维护性和扩展性- 模块化设计:国际化通常涉及到将文本、日期格式、货币符号等资源分离出来,存储在独立的文件或数据库中。这种模块化设计使得系统的维护和扩展更加容易。- 代码复用:国际化的设计模式和工具可以复用,减少重复工作,提高开发效率。
- 一致的用户体验- 统一风格:通过国际化,可以确保不同语言版本的应用具有一致的风格和用户体验,避免因为语言不同而导致的用户体验差异。- 多语言切换:支持用户在应用内切换语言,提供无缝的多语言体验。
- 数据处理和格式化- 日期和时间:不同国家和地区对日期和时间的格式有不同的要求。国际化可以帮助应用正确地显示和处理日期和时间。- 数字和货币:不同地区对数字和货币的表示方式也有所不同。国际化可以确保应用正确地显示和处理这些数据。
2.2 微服务中常用的国际化方法
在微服务设计中,国际化的实现方案有多种,下面列举了常用的几种方案
2.2.1 资源文件分离
将文本、图像等资源分离出来,存储在不同的文件中,每个文件对应一种语言。
2.2.2 使用国际化框架
使用现有的国际化框架,如 Spring Framework 的
ResourceBundleMessageSource
,简化国际化的开发和适配工作。
2.2.3 使用动态模板
在springboot框架中,可以使用模板引擎(如 Thymeleaf、Freemarker)动态生成多语言内容。
2.2.4 使用数据库存储
将多语言内容或关键的配置信息存储在数据库中,根据用户的语言偏好动态加载。
2.2.5 API设计结合配置中心
设计 API 时考虑国际化需求,提供语言参数,可能的话尽量支持将配置文件迁移到配置中心进行控制,从而支持多语言响应。
三、SpringBoot 国际化介绍与实践
3.1 SpringBoot 国际化概述
Spring Boot 提供了强大的国际化(i18n)支持,使得应用程序可以根据用户的语言和地区偏好显示不同的文本。通过使用 Spring 的国际化机制,你可以将应用程序的文本、日期格式、货币符号等内容进行本地化,以适应不同用户的需求。
3.1.1 SpringBoot国际化一般步骤
Spring Boot 国际化基本步骤如下:
- 创建资源文件:将不同语言的文本内容存储在资源文件中。
- 配置消息源:告诉 Spring 如何加载这些资源文件。
- 使用消息源:在控制器、视图等地方使用消息源来获取本地化的文本。
- 设置用户语言:通过
LocaleResolver
设置用户的语言。
3.2 SpringBoot 国际化实现代码演示
3.2.1 前置准备
创建一个springboot工程,并导入下面基本的依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 展示视图使用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>3.1.5</version>
</dependency>
</dependencies>
<build>
<finalName>boot-docker</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.2.2 创建资源文件
创建包含不同语言版本的消息文件,这些文件通常放在 src/main/resources 目录下,并且以
.properties
文件的形式的扩展名存在,比如:
- messages.properties (默认语言,如英语);
- messages_en_US.properties(显示指定英文语言);
- messages_zh_CN.properties (简体中文);
- messages_fr_CN.properties (法语);
如下,在resources 目录下分别创建几个对应的资源文件,各自的内容如下:
messages.properties
welcome.message=Welcome to our website!
error.message=An error occurred.
messages_en_US.properties
welcome.message=Welcome to our website!
error.message=An error occurred.
messages_zh_CN.properties
welcome.message=欢迎来到我们的网站!
error.message=发生了一个错误。
3.2.3 增加一个html模板文件
为了能够清楚看到各种语言文件展示的效果,这里我们通过一个thymeleaf目标文件将上述资源文件中的内容取出来展示到页面上显示,在resources目录下增加一个templates的目录,里面添加一个hello.html的页面,默认情况下,springboot工程会自动读取到该目录下的html文件,html文件内容如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title> hello message</title>
</head>
<body>
<h1 th:text="#{welcome.message}"></h1>
</body>
</html>
3.2.4 增加一个视图配置文件
还需在工程中增加一个配置类,从而让springboot能够解析html的模板文件,参考如下代码:
package com.congge.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/hello").setViewName("hello");
}
}
3.2.5 效果测试与验证
启动springboot工程之后,浏览器访问:http://localhost:8081/hello ,此时将第二步中定义的资源文件内容取了出来。
为什么会展示中文这个资源文件呢?打开浏览器设置可以看到,浏览器默认使用的是中文
切换为英语之后再次访问,可以看到此时就展示为英文效果了
3.3 SpringBoot 国际化底层实现原理
3.3.1 核心配置类说明
在springboot启动加载的时候,与国际化文件相关的一个核心配置类名为 MessageSourceAutoConfiguration,源码如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.context;
import java.time.Duration;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
@AutoConfiguration
@ConditionalOnMissingBean(
name = {"messageSource"},
search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(Integer.MIN_VALUE)
@Conditional({ResourceBundleCondition.class})
@EnableConfigurationProperties
@ImportRuntimeHints({MessageSourceRuntimeHints.class})
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = new Resource[0];
public MessageSourceAutoConfiguration() {
}
@Bean
@ConfigurationProperties(
prefix = "spring.messages"
)
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@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;
}
static class MessageSourceRuntimeHints implements RuntimeHintsRegistrar {
MessageSourceRuntimeHints() {
}
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("messages.properties").registerPattern("messages_*.properties");
}
}
protected static class ResourceBundleCondition extends SpringBootCondition {
private static final ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap();
protected ResourceBundleCondition() {
}
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = (ConditionOutcome)cache.get(basename);
if (outcome == null) {
outcome = this.getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle", new Object[0]);
String[] var4 = StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename));
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
String name = var4[var6];
Resource[] var8 = this.getResources(context.getClassLoader(), name);
int var9 = var8.length;
for(int var10 = 0; var10 < var9; ++var10) {
Resource resource = var8[var10];
if (resource.exists()) {
return ConditionOutcome.match(message.found("bundle").items(new Object[]{resource}));
}
}
}
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}
private Resource[] getResources(ClassLoader classLoader, String name) {
String target = name.replace('.', '/');
try {
return (new PathMatchingResourcePatternResolver(classLoader)).getResources("classpath*:" + target + ".properties");
} catch (Exception var5) {
return MessageSourceAutoConfiguration.NO_RESOURCES;
}
}
}
}
从这个类可以首先得到下面几个信息:
- springboot启动时,该类会被自动加载,类上面有@AutoConfiguration注解;
- MessageSourceProperties会以bean的方式纳入到spring管理;- bean实例化过程中,会读取spring.messages开头的配置信息;- MessageSourceProperties 类中定义了默认的可用于配置语言文件信息的配置属性,开发者可以根据自身的需求重新定义和覆盖配置;- 该类中的有个属性basename = "messages",这也是为何在配置文件中给资源文件起名的时候要以messages为前缀;
比如你不想使用默认的资源文件名称,就可以在yml文件像下面这样配置:
spring:
messages:
basename: language
encoding: GBK
3.4 代码获取国际化资源配置信息
如今前后端分离的开发模式已经在各互联网公司广泛使用,在这种情况下,有时候并不需要服务端直接控制页面的语言环境,但是需要告诉页面与国际化相关的信息,通常来说,就需要服务端接口能够获取到国际化资源配置内容。
3.4.1 使用MessageSource获取资源配置信息
在spring框架中,可以通过MessageSource这个接口提供的方法获取到语言文件中的配置信息,在MessageSource接口中提供了几个获取配置信息的方法,如下:
下面提供一个接口,获取配置信息,参考下面的代码
@Resource
private MessageSource messageSource;
//localhost:8081/getMessage
@GetMapping("/getMessage")
public String getMessage(){
String message = messageSource.getMessage("welcome.message", null, Locale.ENGLISH);
return message;
}
启动工程之后调用下接口,基于当前的浏览器语言环境,可以看到读取到了英文的配置信息
3.5 完整代码演示
下面结合实际项目实践经验,演示一下如何在springboot项目中集成国际化环境
3.5.1 增加配置信息
基于上述的三个resources目录下的资源配置文件,分别添加下面的信息
messages.properties
mess.user.name=玛丽
mess.user.password=密码
mess.user.btn=登录
messages_zh_CN.properties
mess.user.name=玛丽
mess.user.password=密码
mess.user.btn=登录
messages_en_US.properties
mess.user.name=merry
mess.user.password=Password
mess.user.btn=Sign In
3.5.2 自定义语言解析器
该类实现LocaleResolver接口,根据请求中的参数信息解析语言环境,从而匹配本地语言资源文件的内容
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import java.util.Locale;
public class SelfLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(jakarta.servlet.http.HttpServletRequest request) {
String language = request.getHeader("lang");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(language)) {
String[] split = language.split("_");
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, Locale locale) {
}
}
3.5.3 全局配置语言处理器
实现WebMvcConfigurer接口,将上述配置的解析器放到spring上下文管理的容器中
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
public class LocaleConfig implements WebMvcConfigurer {
/**
* 默认解析器 其中locale表示默认语言,当请求中未包含语种信息,则设置默认语种
* 当前默认为CHINA,zh_CN
*/
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
/**
* lang表示切换语言的参数名
* 拦截请求
* 获取请求参数lang种包含的语种信息并重新注册语种信息
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
// 前端请求头中的参数名
lci.setParamName("lang");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
3.5.4 增加i18n工具类
该类用于获取指定的资源文件中的参数值
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import java.util.Locale;
@Slf4j
@Component
public class MessageUtils {
private static MessageSource messageSource;
public MessageUtils(MessageSource messageSource) {
if (log.isInfoEnabled()) {
log.info("MessageUtils construction success");
}
MessageUtils.messageSource = messageSource;
}
/**
* 获取单个国际化值
*/
public static String get(String msgKey) {
Locale locale = LocaleContextHolder.getLocale();
try {
return messageSource.getMessage(msgKey, null, locale);
} catch (Exception e) {
log.error("msgKey:{},locale:{},MessageUtils.get Exception:{}", msgKey, locale, e.getMessage());
return messageSource.getMessage(msgKey, null, Locale.US);
}
}
/**
* 获取指定语言单个国际化的值
*/
public static String get(String msgKey, String language) {
try {
//线上直接new Locale(language),会是小写en_us,调用本地方法读取资源的时候会识别不到,因为配置文件是大写的后缀en_US
//所以这里拆分,然后拼装为大写的Locale的en_US
String[] s = StringUtils.split(language, "_");
Locale locale = new Locale(s[0], s[1]);
if (log.isInfoEnabled()) {
log.info("get,msgKey:{},language:{},locale:{}", msgKey, language, locale);
}
return messageSource.getMessage(msgKey, null, locale);
} catch (Exception e) {
log.error("msgKey:{},language:{},MessageUtils.get Exception:{}", msgKey, language, e.getMessage());
return messageSource.getMessage(msgKey, null, Locale.US);
}
}
}
3.5.5 测试接口
添加如下的测试接口
//localhost:8081/getMessage/v2
@GetMapping("/getMessage/v2")
public String getMessageV2(){
String val = MessageUtils.get("mess.user.name");
return val;
}
测试一:使用中文环境
测试二:使用英文环境
通过上面的这种方式,可以根据不同的业务需求场景,返回不同的语言环境下的配置信息,从而做到国际化适配。
3.6 其他场景补充
基于上述的国际化配置实现方案,在实际开发中,还有类似的其他场景需要进行国际化适配,这里再补充下面几点:
- 异常或错误国际化- 程序抛异常的时候,前端可能需要根据语言环境得到不同的异常提示,服务端需要对异常进行国际化处理;
- 日志国际化- 有一些系统提供了可视化的日志,需要进行国际化处理;
- 邮件通知国际化- 系统中需要发送邮件的场景下,需要根据用户的语言环境进行国际化适配;
配置文件国际化- 配置文件中的注释和说明需要根据用户的语言偏好进行适配。- 配置文件中的某些值(如提示信息、默认值等)需要支持多语言。
四、写在文末
本文详细介绍了springboot国际化的理论和解决方案,并通过案例代码演示了如何在springboot中完成国际化的通用配置,希望对看到的同学有用,本篇到此结束,感谢观看。
版权归原作者 小码农叔叔 所有, 如有侵权,请联系我们删除。