乐趣区

关于spring:SpringBoot基础系列之手把手实现国际化支持实例开发

【SpringBoot 根底系列】手把手实现国际化反对实例开发

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

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

<!– more –>

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. 我的项目

  • 工程:https://github.com/liuyueyi/spring-boot-demo
  • 我的项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/150-i18n

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因集体能力无限,不免有疏漏和谬误之处,如发现 bug 或者有更好的倡议,欢送批评指正,不吝感谢

上面一灰灰的集体博客,记录所有学习和工作中的博文,欢送大家前去逛逛

  • 一灰灰 Blog 集体博客 https://blog.hhui.top
  • 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top

退出移动版