springboot 项目国际化解决方案
一、快速上手springboot国际化
springboot 只是对spring+springmvc等的一个整合,因此在只使用了springMVC的项目中将springboot中相关的实现发生迁移至springMVC下也比较简单
1、编写国际化资源文件
要求:一定要指定properties文件编码格式为utf-8 否则国际化资源文件获取后出现乱码
resourecs目录下创建 static/i18n/国际化资源文件:
- language.properties为默认国际化资源文件内容
- language_en_US.properties: 英文环境下资源文件内容
- language_zh_CN.properties:简体中文下资源文件内容
实例:
# language_zh_CN.properties配置
user.username=用户名
user.password=密码
user.login=登录
user.title=请登录
# language_en_US.properties配置
user.username=username
user.password=password
user.login=login
user.title=please login
可根据国际化要求继续创建更多语言的文件
2、springboot配置读取该资源文件
application.properties中配置:
spring.messages.basename: static/i18n/language,static/i18n/login
只需要指定到默认资源文件前缀即可。如果存在多组资源文件配置用逗号分开即可
3、以thymeleaf方式在html页面中使用为例
login.html编写如下
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登录</title>
</head>
<body>
<h3 th:text="#{user.title}">请登录</h3>
<h4 th:text="#{user.username}"></h4>
<input th:value="${name}" >
<h4 th:text="#{user.password}"></h4>
<input th:value="${password}">
<button><span th:text="#{user.login}"></span> </button>
</body>
</html>
读取国际化配置采用#{msgKey}方式,加载后台viewModule中内容采用${key}
4.controlle实现
@Controller
@RequestMapping("login")
public class LoginController {
@GetMapping("view")
public String loginView(){
return "login";
}
}
5、启动浏览器访问即可
中文环境:
英文环境:
到此我们已经完成一个简单的springboot国际化解决方案了。但是当前模式下默认只是跟随浏览器语言环境。我们经常会看到如的方式来选择语言进行多语言的切换。对于用户来说这也是一种更加友好的切换方式。这样我们就可以采取通过url上绑定语言参数的方式来实现多语言。点击下来选择语言后,默认未用户所有发起的url请求上添加语言参数发起请求。类似于:
http://localhost:8088/login/view?lang=en_US 的方式来进行语言选择。
二、通过绑定url请求参数的方式实现国际化
1、重新定义LocaleResolver bean的创建
/**
* 默认语言bean的创建
* @return
*/
@Bean
public LocaleResolver localeResolver(){
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
}
注意:此处创建的localeResolver bean的id名必须为localeResolver。否则springboot请求时会抛出 【Cannot change HTTP accept header - use a different locale resolution strategy 】 的异常
2、注入参数拦截器
/**
* 默认语言拦截器。lang表示切换语言的参数名
* @return
*/
@Bean
public WebMvcConfigurer localeInterceptor() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
// 指定拦截参数名为lang
localeInterceptor.setParamName("lang");
registry.addInterceptor(localeInterceptor);
}
};
}
3、启动项目后指定不同语言访问
http://localhost:8088/login/view?lang=en_US 英文访问地址
http://localhost:8088/login/view?lang=zh_CN 中文访问地址
该方式实现,再去设置浏览器所属的语言时,将不会改变语言环境
三、后台消息等国际化
以上国际化方案主要都是针对页面的国际化方案,但在现在的项目中基本都是采用前后端分离的架构。那spring所支持的国际化对页面感觉就没有什么作用了?在各种前端框架中比如主流的vue和react等都支持一套自有的国际化方案。但前端获取的很多提示性消息等都需要国际化之后响应给前端。
后台相关消息主要有:
1、提示给前端的,操作提示类消息。比如:
-
程序运行错误的500 通常会提示:系统异常,请联系管理员
-
登录验证时提示,密码错误,注册时提示:用户已存在
2、后台程序处理的相关异常。
3、logger.info等输出的日志信息。
通常后台程序处理相关以及日志输出是没有必要做国际化的。因为将日志等国际化,反而增加了错误排查的难度。相对直接采用中文是更有利于排查错误问题的。
提示性消息异常处理方案:
1、封装后台消息获取工具类
获取国际化消息主要采用 MessageSource接口中的getMessage方法来获取指定的消息
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
参数说明
参数 | 说明 |
---|---|
code | 消息key值,比如:message.tip |
args | 消息中占位位置填充值,比如 message.tip=测试消息,参数{0}。那么传入args第0个消息填充花括号的0这个位置 |
defaultMessage | 默认消息,获取不到消息key时,直接取默认消息 |
locale | 指定语言环境(通过 LocaleContextHolder.getLocale()获取) |
通过封装工具类后,再整个项目中直接使用LangeUtils.get()即可获取指定消息的国际化值
@Component
public class LanguageUtils {
private static MessageSource messageSource;
public LanguageUtils(MessageSource messageSource){
LanguageUtils.messageSource = messageSource;
}
public static String get(String msgKey){
return messageSource.getMessage(msgKey,null, LocaleContextHolder.getLocale());
}
public static String get(String msgKey,Object... objects){
return messageSource.getMessage(msgKey,objects,LocaleContextHolder.getLocale());
}
public static String get(String msgKey,String defaultMsg,Object... objects){
return messageSource.getMessage(msgKey,objects,defaultMsg,LocaleContextHolder.getLocale());
}
}
2、定义异常消息枚举
也可以通过静态常量类的方式来解决msgkey问题,总之此目的为了尽可能减少魔法值的使用
public enum ExceptionEnums {
MESSAGE_SYS_EXCEPTION,
MESSAGE_USER_EXIST,
MESSAGE_SUCCESS
}
3、自定义提示性消息异常
通过自定义异常,来区别哪些异常是用于提示给前端,哪些是我们后台程序中需要处理的异常,方便在接口数据反馈时对异常进行捕获
public class TipException extends RuntimeException {
public TipException(ExceptionEnums exceptionEnums){
super(LanguageUtils.get(exceptionEnums.name()));
}
public TipException(String defaultMessage,ExceptionEnums exceptionEnums,Object... args){
super(LanguageUtils.get(exceptionEnums.name(),defaultMessage,args));
}
public TipException(ExceptionEnums exceptionEnums,Object... args){
super(LanguageUtils.get(exceptionEnums.name(),args));
}
}
4、定义全局controller异常处理handle
定义全局controller异常捕获处理handle是后端常用的一种方案,避免我们在controller层写大量的try-catch异常捕获代码。而让程序员只关心service层的调用关系处理
@RestControllerAdvice
public class ExceptionHandle {
private Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(TipException.class)
public String handleTipException(TipException e){
return e.getMessage();
}
@ExceptionHandler(Exception.class)
public String handleException(Exception e){
// 可打印出具体堆栈信息
logger.error("程序运行异常:"+e.getMessage());
return LanguageUtils.get(ExceptionEnums.MESSAGE_SYS_EXCEPTION);
}
}
5、消息国际化资源配置
# en
MESSAGE_SYS_EXCEPTION=system exception
MESSAGE_USER_EXIST=user[{0}] exist
MESSAGE_SUCCESS=success
# zh
MESSAGE_SYS_EXCEPTION=系统异常
MESSAGE_USER_EXIST=用户[{0}]已经存在
MESSAGE_SUCCESS=成功
6、service层业务逻辑处理测试
@Service
public class LoginService {
public void login(String username){
if("1".equals(username)){
throw new TipException(ExceptionEnums.MESSAGE_USER_EXIST,username);
}else if("2".equals(username)){
int i = 0;
i = 2/i;
}else if("3".equals(username)){
// 登录成功
}
}
}
7、controller将异常捕获交由全局异常handle去处理
@Controller
@RequestMapping("login")
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping("login")
@ResponseBody
public String login(String username){
loginService.login(username);
return LanguageUtils.get(ExceptionEnums.MESSAGE_SUCCESS);
}
}
通过以上处理,就完成一个完整的后台消息提示信息的国际化。当然以上针对返回值等通常还可以进行封装为返回状态码和msg一体的消息等。
以上只能实现整个项目中一些静态消息的国际化,而对于一些存储于数据库的动态数据国际化,则需要从数据库设计表层面去解决国际化问题。如:项目中通过后台维护的菜单,以及一些分类信息的描述。