概述
使用spring boot实现国际化,一般有两种情况,一种是返回网页的,一种是纯粹的rest服务返回json的,前者更多的和类似thymeleaf这样的模板引擎一起工作,后者可以不使用模板引擎直接解析,因为后者相对来说内容要简单得多,大部分情况返回json就够了,下面会包含这两部分内容。
过程
1 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
如果只是单纯的rest服务并且不需要做服务端网页渲染的话,可以移除spring-boot-starter-thymeleaf依赖。
2 多语言选择
要实际国际化,无论怎样肯定首先要有一个地方对于选择哪门语言做出判断,比如自定义如下规则:
客户端请求头中添加请求头x-client-language: zh_CN时,根据该请求头做语言切换
建议在服务端项目中添加过滤器(不限定过滤器,有类似效果即可),读取请求头的语言信息生成Locale对象存到ThreadLocal,LocaleResolver对象的说明在第3步。
import com.github.gitveio.utils.ResourceUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
// like LocaleChangeInterceptor class
@WebFilter(filterName = "langFilter", urlPatterns = "/*")
public class LanguageFilter implements Filter {
@Autowired
private LocaleResolver localeResolver;
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
try {
String lang = ((HttpServletRequest) request).getHeader(REQUEST_LOCALE_HEADER);
localeResolver.setLocale((HttpServletRequest) request, (HttpServletResponse) response, getLocale(lang));
chain.doFilter(request, response);
} finally {
ResourceUtil.clear();
}
}
@Override
public void destroy() {
}
private Locale getLocale(String lang) {
Locale locale = null;
if (StringUtils.isEmpty(lang)) {
locale = defaultLocale;
} else {
int splitIndex = lang.indexOf('-');
if (splitIndex < 0) {
splitIndex = lang.indexOf('_');
}
if (splitIndex < 0) {
locale = new Locale(lang.toLowerCase());
} else {
locale = new Locale(lang.substring(0, splitIndex).toLowerCase(), lang.substring(splitIndex + 1).toUpperCase());
}
}
return locale;
}
private static Locale defaultLocale = new Locale("zh", "CN");
public static final String REQUEST_LOCALE_HEADER = "x-client-language";
}
将locale对象存放在threadlocal中主要是方便后面在业务代码中获取该locale对象,如果是纯粹的rest服务不用返回网页内容的情况,推荐直接放在request的属性列表即可。
import java.util.Locale;
public class ResourceUtil {
private static final ThreadLocal<Locale> LOCALE_STORE = new InheritableThreadLocal();
public static void setLocaleObject(Locale locale) {
LOCALE_STORE.set(locale);
}
public static Locale getLocaleObject() {
Locale l = (Locale) LOCALE_STORE.get();
return l;
}
public static void clear() {
LOCALE_STORE.remove();
}
}
3 多语言选择和spring集成
有第2步的语言选择之后,还需要将选择的Locale存下来方便后面使用。
LocaleResolver接口的resolveLocale方法会被需要获取Locale对象的地方自动调用。
LocaleResolver接口的setLocale方法被第2步的过滤器调用,用来保存生成的Locale对象。
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
public class CustomLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest httpServletRequest) {
return ResourceUtil.getLocaleObject();
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
ResourceUtil.setLocaleObject(locale);
}
}
将CustomLocaleResolver对象放入spring容器中,让spring感知到LocaleResolver,然后通过LocaleResolver获取Locale对象
@SpringBootApplication
@EnableAutoConfiguration
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Bean
public LocaleResolver localeResolver() {
LocaleResolver slr = new CustomLocaleResolver();
return slr;
}
}
4 添加国际化属性文件
messages目录下的文件就是多语言的属性文件,其他messages.properties是默认文件,后缀带en_US的是英文的,带zh_CN的是中文的。
下面的简单的示例:
比如定义一个属性名为view.title,中文是"项目管理",英文是"project management",默认是中文的"项目管理"
messages.properties文件内容:
view.title=\u9879\u76ee\u7ba1\u7406
messages_en_US.properties文件内容:
view.title=project management
messages_zh_CN.properties文件内容:
view.title=\u9879\u76ee\u7ba1\u7406
需要说明的是上面的\u开头的字符串代表的是unicode码,也就是"项目管理"的unicode码为\u9879\u76ee\u7ba1\u7406,编码转换工具
5 项目配置
application.yml文件添加配置:
spring:
messages:
basename: i18n/messages
6 编写测试模板文件
hello.html(默认目录在resources/templates)文件内容:
<html xmlns:th="http://www.thymeleaf.org">
<body>
hello, <span th:text=" #{view.title}">book info</span>
</body>
</html>
7 添加测试controller
hi方法是返回hello.html网页,在服务端网页渲染的场景下使用
test方法是直接返回字符串,在rest服务中这种情况更多一些
@Controller
public class TestController {
@RequestMapping(value = "/hi")
public String hi() {
return "hello";
}
@RequestMapping(value = "/test")
@ResponseBody
public String test() {
return getMessage("view.title");
}
private String getMessage(String key) {
return messageSource.getMessage(key, null, ResourceUtil.getLocaleObject());
}
@Autowired
private MessageSource messageSource;
}
8 测试
$ curl http://localhost:8080/hi -H "x-client-language: zh_CN"
<html>
<body>
hello, <span>项目管理</span>
</body>
</html>
$ curl http://localhost:8080/hi -H "x-client-language: en_US"
<html>
<body>
hello, <span>project management</span>
</body>
</html>
问题
1 controller返回String类型的模板名称无法返回网页,直接返回了字符串给客户端
controller使用@RestController或者controller方法使用@ResponseBody后,返回值会直接写回到客户端,导致客户端直接收到字符串,查看@RestController注解其实就是@Controller+@ResponseBody,也就是说@ResponseBody注解会导致这个问题,解决的话要么直接使用@Controller并且去掉@ResponseBody, 要么controller方法返回ModelAndView,比如:
@RequestMapping(value = "/hello")
public ModelAndView hello() {
ModelAndView view = new ModelAndView();
view.setViewName("hello");
return view;
}