SpringBoot 国际化适配方案使用解决方案
一、前言
随着一个系统的规模做上去之后,国际化的问题就会逐渐暴露出来,简单来说,当你的系统面对的不再是本国的用户,而要面临海外用户时,系统必须要能适配国际化。系统国际化这个事情,互联网经历了多年的发展之后,尤其是移动端的适配方案已经很成熟,本文以微服务项目为例,具体来说,以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中完成国际化的通用配置,希望对看到的同学有用,本篇到此结束,感谢观看。
到此这篇关于SpringBoot 国际化适配方案使用详解的文章就介绍到这了,更多相关SpringBoot 国际化适配内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
POI XSSFSheet shiftRows bug问题解决
这篇文章主要介绍了POI XSSFSheet shiftRows bug问题解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-07-07Spring MVC参数校验详解(关于`@RequestBody`返回`400`)
这篇文章主要介绍了Spring MVC参数校验的相关资料,主要是针对`@RequestBody`返回`400`的问题,文中通过示例代码介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面跟着小编来一起学习学习吧。2017-08-08
最新评论