文章目录
- 1.简介
- 2.MessageSource配置和工具类封装
- 2.1.配置MessageSource相关配置
- 2.2.配置工具类
- 2.3.测试返回国际级文本信息
- 3.不优雅的web调用示例(看看就行,别用)
- 4.优雅使用示例
- 4.1.错误响应消息枚举类
- 4.2.ThreadLocal工具类配置
- 4.2.1.ThreadLocal工具类数据封装
- 4.2.2.往ThreadLocal里存放language字段
- 4.2.3.ThreadLocal内存释放
- 4.3.http返回通用Json响应数据类
- 4.4.优化章节3的示例
- 5.各国的国际化文件规范命名列表
- 6.项目配套代码
1.简介
由于公司业务需求,需要支持中英两个版本的返回提示信息,使用spring的MessageSource类获取对应语种的i18n文件的提示信息。
i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称。在资讯领域,国际化(i18n)指让产品(无需做大的改变就能够适应不同的语言和地区的需要。对程序来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面。 在全球化的时代,国际化尤为重要,因为产品的潜在用户可能来自世界的各个角落。通常与i18n相关的还有L10n(“本地化”的简称)。
2.MessageSource配置和工具类封装
为节省代码量使用了lombok插件,在pom.xml引入
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
查看MessageSource源码可以看到该接口提供了三个方法,我们使用第二个即可。
public interface MessageSource {
@Nullable
String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);
/**
* 根据key去国际化对应的文件中找文本信息
* @param var1 key
* @param var2 占位符参数
* @param var3 国际化语种
* @return 对应语种的文本信息
*/
String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException;
}
2.1.配置MessageSource相关配置
@Configuration
public class LocaleConfig {
@Bean
public ResourceBundleMessageSource messageSource() {
Locale.setDefault(Locale.CHINA);
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
//设置国际化文件存储路径和名称 i18n目录,messages文件名
source.setBasenames("i18n/messages");
//设置根据key如果没有获取到对应的文本信息,则返回key作为信息
source.setUseCodeAsDefaultMessage(true);
//设置字符编码
source.setDefaultEncoding("UTF-8");
return source;
}
}
在resources目录下添加国际化文件
en_US存放英语,zh_CH存放中文
记得把idea的文件编码格式改成utf8,要不然中文会乱码。
在代码里用枚举类维护使用的语种
@Getter
@AllArgsConstructor
public enum LanguageEnum {
ZH_CN("zh_CN", "中文/中国"),
EN_US( "en_US", "英语/美国"),
;
/**
* 语言_国家缩写
*/
private String name;
/**
* 描述
*/
private String desc;
}
2.2.配置工具类
下面代码封装了两个对外暴露的方法
- get(String key, String language) 根据key获取文本信息
- get(String key, Object[] params, String language) 根据key获取替换占位符中的文本信息
MessageSource的实例对象这是通过内部类懒加载的方式创建
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MessageUtil {
/**
* 根据key信息获取对应语言的内容
*
* @param key 消息key值
* @param language 语言_国家
* @return
*/
public static String get(String key, String language) {
if (!StringUtils.isEmpty(language)) {
String[] arrs = language.split("_");
if (arrs.length == 2) {
return get(key, new Locale(arrs[0], arrs[1]));
}
}
//使用默认的国际化语言
return get(key, Locale.getDefault());
}
/**
* 根据key信息获取对应语言的内容
*
* @param key 消息key值
* @param params 需要替换到占位符中的参数 占位符下标重0开始 格式如: {0} {1}
* @param language 语言_国家
* @return
*/
public static String get(String key, Object[] params, String language) {
if (!StringUtils.isEmpty(language)) {
String arrs[] = language.split("_");
if (arrs.length == 2) {
return get(key, params, new Locale(arrs[0], arrs[1]));
}
}
return get(key, params, Locale.getDefault());
}
private static String get(String key, Locale language) {
return get(key, new String[0], language);
}
private static String get(String key, Object[] params, Locale language) {
return getInstance().getMessage(key, params, language);
}
private static MessageSource getInstance() {
return Lazy.messageSource;
}
/**
* 使用懒加载方式实例化messageSource国际化工具
*/
private static class Lazy {
private static final MessageSource messageSource = SpringUtil.getBean(MessageSource.class);
}
}
MessageUtil 用到的工具类代码如下
/**
* @author Dominick Li
* @description 普通类调用Spring bean对象使用的工具栏
**/
@Component
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
/**
* 通过class获取Bean.
*/
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
2.3.测试返回国际级文本信息
分别在国际化文件中添加key名称为test的项,并添加{0} 占位符支持动态参数
messages_en_US.properties文件内容如下
test=language:{0} ,Hello World!
messages_zh_CN.properties内容如下
test=语言:{0} ,你好世界
测试代码如下
@RestController
public class TestI18nController {
/**
* 测试国际化
* en_US 英文 http://127.0.0.1:8033/en_US
* zh_CN 中文 http://127.0.0.1:8033/zh_CN
*/
@GetMapping("/{language}")
public String test(@PathVariable String language) {
String text1 = MessageUtil.get("test", language);
String text2 = MessageUtil.get("test", new Object[]{language}, language);
return new StringBuilder("没有替换占位符参数:")
.append(text1)
.append("<br/>")
.append("替换占位符参数后:")
.append(text2)
.toString();
}
}
用浏览器访问http://127.0.0.1:8033/zh_CN 查看中文国际化信息,访问http://127.0.0.1:8033/en_US 查看英文国际化信息
3.不优雅的web调用示例(看看就行,别用)
像一些固定的参数我们通常会放在Header参数里,例如token,同理我们也可以把language字段放到header里,用于接受客户端选择使用的国际化语言。
Controller和Service的代码如下
@RestController
public class TestI18nController {
@Autowired
private LoginService loginService;
/**
* 不优雅登录
*/
@PostMapping("/login")
public JsonResult login(@RequestHeader String language,@RequestParam String username, @RequestParam String password) {
return loginService.login(language,username,password);
}
}
public interface LoginService {
JsonResult login(String language,String username,String password);
}
@Service
public class LoginServiceImpl implements LoginService {
@Override
public JsonResult login(String language, String username, String password) {
if ("123456".equals(password)) {
//通过key和语言找到对应文本的提示信息
return JsonResult.successResult(MessageUtil.get("return.loginOk", language));
}
return JsonResult.failureResult(MessageUtil.get("return.pwd_error", language));
}
}
在postman中调用测试接口
把Header参数中的language改成zh_CN
上述代码的问题在于
- 1.国际化使用的language字段需要从controller传递到具体的service类中方法中
- 2.在每次通过JsonResult返回提示信息的时候都需要显示调用MessageUtil获取对应文本信息
4.优雅使用示例
- 1.章节3的language字段上下文传递的问题可以通过ThreadLocal的,同理token字段也可以放在ThreadLocal里。
- 2.显示调用MessageUtil工具类获取提示信息可以封装成响应消息枚举类中使用。
4.1.错误响应消息枚举类
@Getter
public enum ReturnMessageEnum {
OK("ok"),
FAILED("failed"),
LOGIN_OK("loginOk"),
PASSWORD_ERROR("pwd_error"),
ACCOUNT_LOCK("account_lock"),
;
/**
* 国际化信息文件里的Key前缀
*/
private final static String prefix = "return.";
/**
* 返回的国际化key信息
*/
private String key;
ReturnMessageEnum(String key) {
this.key = prefix + key;
}
@Override
public String toString() {
return MessageUtil.get(key, ThreadLocalManagerUtil.getLanguage());
}
/**
* 替换占位符中的参数
*
* @param params 需要替换的参数值 长度可变
*/
public String setAndToString(Object... params) {
return MessageUtil.get(key, params, ThreadLocalManagerUtil.getLanguage());
}
}
国际化文件里也需要添加对应的数据
messages_zh_CN.properties文件
return.ok=操作成功!
return.failed=操作失败!
return.loginOk=登录成功!
return.pwd_error=密码错误!
return.account_lock=账号已被锁定{0}分钟,请稍后重试!
messages_en_US.properties
return.ok=Success!
return.failed=Fail!
return.loginOk=Login succeeded!
return.pwd_error=PW error!
return.account_lock=The account has been locked for {0} minutes. Please try again later!
4.2.ThreadLocal工具类配置
系统参数常量类
/**
* 系统常量
*/
public interface SystemConstants {
/**
* 国际化
*/
String LANGUAGE = "language";
/**
* token名称
*/
String TOKEN_NAME = "token";
}
4.2.1.ThreadLocal工具类数据封装
public class ThreadLocalManagerUtil {
@Data
public static class HeaderInfo {
/**
* 用户的token信息
*/
private String token;
/**
* 国际化语言包名称
*/
private String language;
}
/**
* 存储请求头信息
*/
private final static ThreadLocal<HeaderInfo> headerInfoThreadLocal = new ThreadLocal<>();
public static void add(HeaderInfo headerInfo) {
headerInfoThreadLocal.set(headerInfo);
}
public static String getToken() {
return headerInfoThreadLocal.get().getToken();
}
public static String getLanguage() {
return headerInfoThreadLocal.get() != null ? headerInfoThreadLocal.get().getLanguage() : LanguageEnum.ZH_CN.getName();
}
/**
* 释放资源
*/
public static void remove() {
headerInfoThreadLocal.remove();
}
}
4.2.2.往ThreadLocal里存放language字段
使用过空滤器拦截接口请求,并从请求头中获取language字段存放到ThreadLocal中
@Component
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
ThreadLocalManagerUtil.HeaderInfo headerInfo = new ThreadLocalManagerUtil.HeaderInfo();
headerInfo.setLanguage(request.getHeader(SystemConstants.LANGUAGE));
headerInfo.setToken(request.getHeader(SystemConstants.TOKEN_NAME));
//在ThreadLocal中添加token和当前国际化信息
ThreadLocalManagerUtil.add(headerInfo);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
4.2.3.ThreadLocal内存释放
如果ThreadLocal存放的数据不释放,当用户量大的时候会导致系统出现OOM问题
添加拦截器在请求结束的时候释放内存。
public class HttpInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//会话结束移除线程缓存
ThreadLocalManagerUtil.remove();
return;
}
}
注册拦截器到webMVC中
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry interceptor) {
//需要配置拦截器并指定拦截的接口路径
interceptor.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
}
}
4.3.http返回通用Json响应数据类
public class JsonResult<T> implements Serializable {
/**
* 成功标识 200成功,其它异常
*/
private int code = 200;
/**
* 提示信息
*/
private String msg;
/**
* 数据
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private T data;
private static final long serialVersionUID = -7268040542410707954L;
public JsonResult() {
}
public JsonResult(int code) {
this.setCode(code);
}
public JsonResult(int code, String msg) {
this(code);
this.setMsg(msg);
}
public JsonResult(int code, String msg, T data) {
this(code, msg);
this.setData(data);
}
public static JsonResult build(boolean flag) {
return new JsonResult(flag ? HttpStatus.OK.value() : HttpStatus.INTERNAL_SERVER_ERROR.value(), flag ? ReturnMessageEnum.OK.toString() : ReturnMessageEnum.FAILED.toString());
}
public static JsonResult successResult() {
return new JsonResult(HttpStatus.OK.value(), ReturnMessageEnum.OK.toString());
}
public static JsonResult successResult(String msg) {
return new JsonResult(HttpStatus.OK.value(), defaultSuccessMsg(msg));
}
public static <T> JsonResult<T> successResult(T obj) {
return new JsonResult(HttpStatus.OK.value(), ReturnMessageEnum.OK.toString(), obj);
}
public static <T> JsonResult<T> successResult(String msg, T obj) {
return new JsonResult(HttpStatus.OK.value(), defaultSuccessMsg(msg), obj);
}
public static JsonResult failureResult() {
return new JsonResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), ReturnMessageEnum.FAILED.toString());
}
public static JsonResult failureResult(String msg) {
return new JsonResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), defautlErrorMsg(msg));
}
public static JsonResult failureResult(ReturnMessageEnum returnMessageEnum) {
return new JsonResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), returnMessageEnum.toString());
}
protected static String defautlErrorMsg(String msg) {
if (!StringUtils.isEmpty(msg)) {
return msg;
} else {
return ReturnMessageEnum.FAILED.toString();
}
}
protected static String defaultSuccessMsg(String msg) {
if (!StringUtils.isEmpty(msg)) {
return msg;
} else {
return ReturnMessageEnum.OK.toString();
}
}
}
4.4.优化章节3的示例
@RestController
public class TestI18nController {
@Autowired
private LoginService loginService;
/**
* 优化后的登录接口
*/
@PostMapping("/login")
public JsonResult login(@RequestParam String username, @RequestParam String password) {
return loginService.login(username,password);
}
}
public interface LoginService {
JsonResult login(String username,String password);
}
@Service
public class LoginServiceImpl implements LoginService {
@Override
public JsonResult login(String username, String password) {
if ("123456".equals(password)) {
return JsonResult.successResult(ReturnMessageEnum.LOGIN_OK);
}
//使用枚举替换占位符内容后的信息
System.out.println(ReturnMessageEnum.ACCOUNT_LOCK.setAndToString(3));
return JsonResult.failureResult(ReturnMessageEnum.PASSWORD_ERROR);
}
}
使用postman调用接口测试可以实现同章节3一样的效果。
改成英语后调用也是一样的。
使用枚举类替换占位符内容后的信息也打印在控制台上面了。
5.各国的国际化文件规范命名列表
命名规则 语种_地区,中国,香港,台湾都属于中国,所以前面都是zh开头,_后面则表示地区
中国 : zh_CN
香港 : zh_HK
台湾:zh_TW
日本 : ja_JP
美国 : en_US
英国 : en_GB
秘鲁 : es_PE
巴拿马 : es_PA
波斯尼亚和黑山共和国 : sr_BA
危地马拉 : es_GT
阿拉伯联合酋长国 : ar_AE
挪威 : no_NO
阿尔巴尼亚 : sq_AL
伊拉克 : ar_IQ
也门 : ar_YE
葡萄牙 : pt_PT
塞浦路斯 : el_CY
卡塔尔 : ar_QA
马其顿王国 : mk_MK
瑞士 : de_CH
芬兰 : fi_FI
马耳他 : en_MT
斯洛文尼亚 : sl_SI
斯洛伐克 : sk_SK
土耳其 : tr_TR
沙特阿拉伯 : ar_SA
塞尔维亚及黑山 : sr_CS
新西兰 : en_NZ
挪威 : no_NO
立陶宛 : lt_LT
尼加拉瓜 : es_NI
爱尔兰 : ga_IE
比利时 : fr_BE
西班牙 : es_ES
黎巴嫩 : ar_LB
加拿大 : fr_CA
爱沙尼亚 : et_EE
科威特 : ar_KW
塞尔维亚 : sr_RS
墨西哥 : es_MX
苏丹 : ar_SD
印度尼西亚 : in_ID
乌拉圭 : es_UY
拉脱维亚 : lv_LV
巴西 : pt_BR
叙利亚 : ar_SY
多米尼加共和国 : es_DO
瑞士 : fr_CH
印度 : hi_IN
委内瑞拉 : es_VE
巴林 : ar_BH
菲律宾 : en_PH
突尼斯 : ar_TN
奥地利 : de_AT
荷兰 : nl_NL
厄瓜多尔 : es_EC
约旦 : ar_JO
冰岛 : is_IS
哥伦比亚 : es_CO
哥斯达黎加 : es_CR
智利 : es_CL
埃及 : ar_EG
南非 : en_ZA
泰国 : th_TH
希腊 : el_GR
意大利 : it_IT
匈牙利 : hu_HU
爱尔兰 : en_IE
乌克兰 : uk_UA
波兰 : pl_PL
卢森堡 : fr_LU
比利时 : nl_BE
印度 : en_IN
西班牙 : ca_ES
摩洛哥 : ar_MA
玻利维亚 : es_BO
澳大利亚 : en_AU
新加坡 : zh_SG
萨尔瓦多 : es_SV
俄罗斯 : ru_RU
韩国 : ko_KR
阿尔及利亚 : ar_DZ
越南 : vi_VN
黑山 : sr_ME
利比亚 : ar_LY
白俄罗斯 : be_BY
以色列 : iw_IL
保加利亚 : bg_BG
马耳他 : mt_MT
巴拉圭 : es_PY
法国 : fr_FR
捷克共和国 : cs_CZ
瑞士 : it_CH
罗马尼亚 : ro_RO
波多黎哥 : es_PR
加拿大 : en_CA
德国 : de_DE
卢森堡 : de_LU
阿根廷 : es_AR
马来西亚 : ms_MY
克罗地亚 : hr_HR
新加坡 : en_SG
阿曼 : ar_OM
泰国 : th_TH
瑞典 : sv_SE
丹麦 : da_DK
洪都拉斯 : es_HN
由于前段时间买相机学习摄影相关的知识,导致博客很久没更新了,还是不能忘记初心,要时刻学习,在这个社会一日不学习就会退步,因为会被同行卷下去。。
6.项目配套代码
github地址
创作不易,要是觉得我写的对你有点帮助的话,麻烦在github上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。
- 1.搭建第一个springboot项目
- 2.Thymeleaf模板引擎实战
- 3.优化代码,让代码更简洁高效
- 4.集成jta-atomikos实现分布式事务
- 5.分布式锁的实现方式
- 6.docker部署,并挂载配置文件到宿主机上面
- 7.项目发布到生产环境
- 8.搭建自己的spring-boot-starter
- 9.dubbo入门实战
- 10.API接口限流实战
- 11.Spring Data Jpa实战
- 12.使用druid的monitor工具查看sql执行性能
- 13.使用springboot admin对springboot应用进行监控
- 14.mybatis-plus实战
- 15.使用shiro对web应用进行权限认证
- 16.security整合jwt实现对前后端分离的项目进行权限认证
- 17.使用swagger2生成RESTful风格的接口文档
- 18.使用Netty加websocket实现在线聊天功能
- 19.使用spring-session加redis来实现session共享
- 20.自定义@Configuration配置类启用开关
- 21.对springboot框架编译后的jar文件瘦身
- 22.集成RocketMQ实现消息发布和订阅
- 23.集成smart-doc插件零侵入自动生成RESTful格式API文档
- 24.集成FastDFS实现文件的分布式存储
- 25.集成Minio实现文件的私有化对象存储
- 26.集成spring-boot-starter-validation对接口参数校验
- 27.集成mail实现邮件推送带网页样式的消息
- 28.使用JdbcTemplate操作数据库
- 29.Jpa+vue实现单模型的低代码平台
- 30.使用sharding-jdbc实现读写分离和分库分表
- 31.基于分布式锁或xxl-job实现分布式任务调度
- 32.基于注解+redis实现表单防重复提交
- 33.优雅集成i18n实现国际化信息返回