【SpringBoot基础系列】手把手实现国际化支持实例开发

国际化的支持,对于app开发的小伙伴来说应该比价常见了;作为java后端的小伙伴,一般来讲接触国际化的机会不太多,毕竟业务开展到海外的企业并没有太多

SpringBoot提供了国际化的支持,网上也有相关的教程,然而实际体验的时候,发现并没有预期的那么顺利;本文将介绍一下SpringBoot如何支持国家化,以及在支持的过程中,一些注意事项

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

<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>
</dependency>
</dependencies>

2. 配置文件

配置文件中,指定国际化的参数,thmeleaf的配置信息

application.yml

spring:
messages:
basename: i18n/messages/messages
encoding: UTF-8
fallbackToSystemLocale: false thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false

3. 国际化信息文件

上面的配置 spring.messages.basename 指定国际化配置文件的目录与前缀,取值为i18n/messages/messages

所以在资源目录下,新建文件 i18n/messages,国际化文件名为 messages-xxx.properties,项目结果如

对应的信息如简体中文 messages_zh_CN.properties

200=成功
500=內部异常
name=用户名
pwd=密码

英文 messages_en_US.properties

200=success
500=unexpected exception
name=user name
pwd=password

繁体 messages_zh_TW.properties

200=成功
500=內部異常
name=用戶名
pwd=密碼

说明

注意spring.messages.basename 这个配置的取值为国际化文件的目录 + 文件名前缀,比如上面若少了最后一层的messages,会提示取不到配置

其次在IDEA中,选中国家化文件之后,点击下方的Resource Bundle,可以进入如上图中更友好的编辑框,支持一次修改多个语言的信息

II. 国际化支持

前面是国际化的基本配置,那么如何根据前面配置中的key,获取不同语言的value呢?

1. MessageSource

在SpringBoot中主要借助MessageSource来获取不同语言的value信息

如一个最基本的封装

public class MsgUtil {
private static MessageSource messageSource; public static void inti(MessageSource messageSource) {
MsgUtil.messageSource = messageSource;
} /**
* 获取单个国际化翻译值
*/
public static String get(String msgKey) {
try {
return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
} catch (Exception e) {
return msgKey;
}
}
}

2. 测试demo

接下来写一个基础的测试demo,根据传参来修改LocalContextHolder中的值,从而实现不同语言的切换

@Controller
@SpringBootApplication
public class Application { public Application(MessageSource messageSource) {
MsgUtil.inti(messageSource);
} public static void main(String[] args) {
SpringApplication.run(Application.class);
} @Data
@Accessors(chain = true)
public static class RspWrapper<T> {
private int code;
private String msg;
private T data;
} @GetMapping(path = "change")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}
}

演示如下

3. 子线程支持

上面虽然可以根据请求参数来切换语言,但是有个问题,如果在子线程中进行国际化支持,则会不生效

@GetMapping(path = "change2")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1])); RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}

如下图,即便修改了language,返回都是默认的中文

针对这种解决办法是在设置Locale时,指定第二个可继承参数为true

@GetMapping(path = "change3")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}

4. Cookies方式缓存国际化信息

上面虽说支持了根据传参来设置国际化,但是需要每次传参都带上这个参数language=zh_CN,还需要我们自己来解析这个请求参数,我们可以考虑借助拦截器来实现统一的Local设置

这个拦截器可以自己按照上面的方式写,当然更推荐的是直接使用已封装好的

@Configuration
public class AutoConfig implements WebMvcConfigurer {
/**
* 这个如果不存在,则会抛异常: nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy
*
* @return
*/
@Bean
public LocaleResolver localeResolver() {
// 也可以换成 SessionLocalResolver, 区别在于国际化的应用范围
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
} /**
* 根据请求参数,来设置本地化
*
* @return
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
// Defaults to "locale" if not set
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
} @Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
interceptorRegistry.addInterceptor(localeChangeInterceptor());
}
}

请注意上面的 localResolver, 当我们不注册这个bean的时候,运行则会抛出异常nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution

上面的实例中,采用的是CookieLocaleResolver,因此会在cookie中缓存语言信息,一次修改,后续都会生效

测试如下

@GetMapping(path = "say")
@ResponseBody
public String say(String name) {
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
return JSON.toJSONString(res);
} @GetMapping(path = "say2")
@ResponseBody
public String say2(String name) {
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
return JSON.toJSONString(res);
}

主要一个地方设置了语言,后续的访问不带语言参数时,都会复用之前设置的语言,这样使用来说就更简洁了

5. 页面元素国际化

上面介绍的是返回的json串支持国际化,另外一个场景就是我们返回的页面,希望渲染的数据也可以实现国际化支持

在上文的基础上实现这个也没什么难度了

在资源目录下,新建目录templates,新建模板文件 index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>一灰灰blog 国际化测试页面</title>
</head>
<body> <div>
<div class="title">hello world!</div>
<br/>
<div class="content" th:text="'name: ' + ${name}">默认用户名</div>
<br/>
<div class="sign" th:text="'pwd: ' + ${pwd}">默认密码</div>
<br/>
</div>
</body>
</html>

对应的controller

@GetMapping(path = {"", "/", "/index"})
public String index(Model model) {
model.addAttribute("name", MsgUtil.get("name"));
model.addAttribute("pwd", MsgUtil.get("pwd"));
return "index";
}

虽说上面这样实现了国家化的支持,但是看起来不太优雅,难道还需要后端接口进行转义一下么,没有更简单的方式么?

Themeleaf提供了更简单的支持方式,将上面的$改成#即可

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>一灰灰blog 国际化测试页面</title>
</head>
<body> <div>
<div class="title">hello world!</div>
<br/>
<div class="content" th:text="'name: ' + #{name}">默认用户名</div>
<br/>
<div class="sign" th:text="'pwd: ' + #{pwd}">默认密码</div>
<br/>
<div class="content" th:text="'200: ' + #{200}">200</div>
<br/>
<div class="content" th:text="'500: ' + #{500}">500</div>
</div>
</body>
</html>

对应的rest

@GetMapping(path = "show")
public String show() {
return "show";
}

6. 注意事项

在实现国际化的过程中,遇到了下面几个问题,特此记录一下

6.1 配置信息无法获取

在使用messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale())查询配置信息,结果提示org.springframework.context.NoSuchMessageException: No message found under code '200' for locale 'en_US'.

出现上面这个问题,当然优先判断是否真的配置了这个参数,其次确认spring.messages.basename是否准确,对应的value为目录 + 语言的前缀

  • 如我的配置文件为 i18n/messages/messages_en_US.properties, 那么这个value就应该是 i18n/messages/messages

6.2 中文乱码问题

  • 设置编码 spring.messages.encoding=utf-8

如果发现上面这个设置了依然没有生效,那么考虑一下配置文件是否为utf-8编码

6.3 根据请求支持国际化

需要添加本地化的拦截器LocaleChangeInterceptor,来实现根据请求参数,解析语言环境

其次需要注册LocaleResolver,比如demo中使用CookieLocaleResolver,来保存国际化信息 (如果不设置它会抛异常)

II. 其他

0. 项目

1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

【SpringBoot基础系列】手把手实现国际化支持实例开发的更多相关文章

  1. SpringBoot基础系列之自定义配置源使用姿势实例演示

    [SpringBoot基础系列]自定义配置源的使用姿势介绍 前面一篇博文介绍了一个@Value的一些知识点,其中提了一个点,@Value对应的配置,除了是配置文件中之外,可以从其他的数据源中获取么,如 ...

  2. 【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)

    [SpringBoot 基础系列]实现一个自定义配置加载器(应用篇) Spring 中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量:某些时候,我们的配置可 ...

  3. SpringBoot基础系列-SpringCache使用

    原创文章,转载请标注出处:<SpringBoot基础系列-SpringCache使用> 一.概述 SpringCache本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用Sprin ...

  4. SpringBoot基础系列-使用日志

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9996897.html SpringBoot基础系列-使用日志 概述 SpringBoot ...

  5. SpringBoot基础系列-使用Profiles

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9996884.html SpringBoot基础系列-使用Profile 概述 Profi ...

  6. SpringBoot基础系列-SpringBoot配置

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9990680.html SpringBoot基础系列-SpringBoot配置 概述 属性 ...

  7. 【SpringBoot基础系列-实战】如何指定 bean 最先加载(应用篇)

    [基础系列-实战]如何指定 bean 最先加载(应用篇) 在日常的业务开发中,绝大多数我们都是不关注 bean 的加载顺序,然而如果在某些场景下,当我们希望某个 bean 优于其他的 bean 被实例 ...

  8. SpringBoot基础系列一

    SpringBoot基础知识概览 特性 核心理念:约定优于配置 特点: 1. 开箱即用,根据项目依赖自动配置 2. 功能强大的服务体系,如嵌入式服务.安全 3. 绝无代码生成,不用写.xml配置,用注 ...

  9. springBoot基础系列--properties配置

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/7183408.html SpringBoot中免除了大部分手动配置,但是对于一些特定的情况, ...

随机推荐

  1. 数据结构之队列(JavaScript描述)

    队列数据结构   队列遵循先进先出原则的一组有序的项.对可在尾部添加新元素并从顶部移除元素.最新添加的元素必须排在队列的末尾 队列类似栈的例子 创建队列 创建一个类表示队列 队列内应该有一些方法 添加 ...

  2. Pyqt5 combobox

    起因 combobox的使用和介绍 两个combobox 联动 开始 介绍 Combobox是Qt中的下拉复选框, 注意:在添加列表选项时,可以一个个添加,也可以直接使用列表一次性添加多个: 添加多个 ...

  3. ubuntu 1804 配置阿里源

    以防出错,先备份sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak编辑元列表文件sudo vim /etc/apt/sources.list ...

  4. hdu5033 最大仰望角

    题意:       给你n个楼房排成一条直线,楼房可以看成是宽度为1的线段,然后给你m组询问,每组询问给你一个坐标,输出在当前坐标仰望天空的可视角度. 思路:       n比较大,O(n*m)肯定跪 ...

  5. Python中sys模块的使用

    目录 sys模块 sys.argv() sys.exit(0) sys.path sys.modules sys模块负责程序与python解释器的交互,提供了一系列的函数和变量,用于操控python的 ...

  6. Shodan的使用

    目录 Shodan Shodan工作原理 Shodan的使用 使用搜索过滤 Kali中安装 Shodan Kali中Shodan的使用 Shodan Shodan 是一个搜索引擎,但它与 Google ...

  7. python中实现打印特定字符变换

    首先需要将 lib文件 放在该文件同一目录 使用的时候,先导入 from lib.common import print_msg ,然后调用里面的 print_msg() 方法即可! lib文件地址: ...

  8. Win64 驱动内核编程-3.内核里使用内存

    内核里使用内存 内存使用,无非就是申请.复制.设置.释放.在 C 语言里,它们对应的函数是:malloc.memcpy.memset.free:在内核编程里,他们分别对应 ExAllocatePool ...

  9. android 代码中使用textAppearance

    一开始在代码中我以为使用tvAge.setTextAppearance(context, resid);这样的的方式就能行, 运行之后发现这个设置并未生效,于是到处搜索在代码中设置系统样式的的解决方法 ...

  10. 在kubernetes上运行WASM负载

    在kubernetes上运行WASM负载 WASM一般用在前端业务中,但目前有扩展到后端服务的趋势.本文使用Krustlet 将WASM服务部署到kubernetes. 简介 Krustlet 是一个 ...