随着互联网的迅速普及,目前(2019)国内的越来越多网络公司都开始走出国门,开辟海外市场,然而在出海之前需要解决的重大问题之一就是多语言环境。以往只有大型的跨国公司才会在项目中使用多语言国际化方案,现在这样的国际化应用将会越来越多。本文将介绍在 Spring Boot 项目中提供 RESTful 风格 API 的多语言国际化(i18n)解决方案
将需要用到的语言添加到此类中
../demo-common/src/main/java/com/ljq/demo/springboot/common/i18n/LanguageEnum.java
package com.ljq.demo.springboot.common.i18n;
import lombok.Getter;
import lombok.ToString;
import org.springframework.util.StringUtils;
/**
* @Description: 国际化 语言枚举类
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Getter
@ToString
public enum LanguageEnum {
/**
* 美式英文
*/
LANGUAGE_EN_US("en_us"),
/**
* 简体中文
*/
LANGUAGE_ZH_CN("zh_cn");
private String language;
private LanguageEnum(String language){
this.language = language;
}
/**
* 获取指定语言类型(如果没有对应的语言类型,则返回中文)
*
* @param language 语言类型
* @return
*/
public static String getLanguageType(String language){
if (StringUtils.isEmpty(language)) {
return LANGUAGE_ZH_CN.language;
}
for (LanguageEnum languageEnum : LanguageEnum.values()) {
if (languageEnum.language.equalsIgnoreCase(language)) {
return languageEnum.language;
}
}
return LANGUAGE_ZH_CN.language;
}
}
../demo-common/src/main/java/com/ljq/demo/springboot/common/i18n/I18nMessageUtil.java
package com.ljq.demo.springboot.common.i18n;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import java.io.IOException;
/**
* @Description: 多语言国际化消息工具类
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
public class I18nMessageUtil {
private static MessageSourceAccessor accessor;
private static final String PATH_PARENT = "classpath:i18n/";
private static final String SUFFIX = ".properties";
private static ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
private I18nMessageUtil(){
}
/**
* 初始化资源文件的存储器
* 加载指定语言配置文件
*
* @param language 语言类型(文件名即为语言类型,eg: en_us 表明使用 美式英文 语言配置)
*/
private static void initMessageSourceAccessor(String language) throws IOException {
/**
* 获取配置文件名
*/
Resource resource = resourcePatternResolver.getResource(PATH_PARENT + language + SUFFIX);
String fileName = resource.getURL().toString();
int lastIndex = fileName.lastIndexOf(".");
String baseName = fileName.substring(0,lastIndex);
/**
* 读取配置文件
*/
ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
reloadableResourceBundleMessageSource.setBasename(baseName);
reloadableResourceBundleMessageSource.setCacheSeconds(5);
reloadableResourceBundleMessageSource.setDefaultEncoding("UTF-8");
accessor = new MessageSourceAccessor(reloadableResourceBundleMessageSource);
}
/**
* 获取一条语言配置信息
*
* @param language 语言类型,zh_cn: 简体中文, en_us: 英文
* @param message 配置信息属性名,eg: api.response.code.user.signUp
* @param defaultMessage 默认信息,当无法从配置文件中读取到对应的配置信息时返回该信息
* @return
* @throws IOException
*/
public static String getMessage(String language, String message, String defaultMessage) throws IOException {
initMessageSourceAccessor(language);
return accessor.getMessage(message,defaultMessage,LocaleContextHolder.getLocale());
}
}
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCodeI18n.java
package com.ljq.demo.springboot.common.api;
import lombok.Getter;
import lombok.ToString;
/**
* @Description: 多语言,国际化接口返回码枚举
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Getter
@ToString
public enum ResponseCodeI18n {
SUCCESS(1000, "api.response.code.success"),
FAIL(-1, "api.response.code.fail"),
// 公共参数
PARAM_ERROR(1001, "api.response.code.paramError"),
LANGUAGE_TYPE_ERROR(1002, "api.response.code.languageTypeError"),
// 用户模块
// 帐号
ACCOUNT_NULL_ERROR(2001,"api.response.code.user.accountNullError"),
ACCOUNT_FORMAT_ERROR(2002, "api.response.code.user.accountFormatError"),
ACCOUNT_NOT_EXIST(2003,"api.response.code.user.accountNotExist"),
ACCOUNT_EXIST(2004,"api.response.code.user.accountExist"),
// 密码
PASSWORD_NULL_ERROR(2101, "api.response.code.user.passwordNullError"),
PASSWORD_FORMAT_ERROR(2102, "api.response.code.user.passwordFormatError"),
PASSWORD_ERROR(2103,"api.response.code.user.passwordError"),
UNKNOWN_ERROR(-1000,"api.response.code.unknownError");
// 返回码
private int code;
// 返回信息
private String msg;
private ResponseCodeI18n(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ApiResultI18n.java
package com.ljq.demo.springboot.common.api;
import com.ljq.demo.springboot.common.i18n.I18nMessageUtil;
import com.ljq.demo.springboot.common.i18n.LanguageEnum;
import lombok.Data;
import java.io.IOException;
import java.io.Serializable;
/**
* @Description: 多语言, 国际化接口返回结果封装
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Data
public class ApiResultI18n implements Serializable {
private static final long serialVersionUID = 4518290031778225230L;
/**
* 返回码,1000 正常
*/
private int code = 1000;
/**
* 返回信息
*/
private String msg = "成功";
/**
* 返回数据
*/
private Object data;
/**
* api 返回结果
*/
private ApiResultI18n() {}
/**
* api 返回结果,区分多语言
*
* @param language 语言类型,eg: en_us 表示美式英文
*/
public ApiResultI18n(String language){
this.code = ResponseCodeI18n.SUCCESS.getCode();
language = LanguageEnum.getLanguageType(language);
try {
this.msg = I18nMessageUtil.getMessage(language,ResponseCodeI18n.SUCCESS.getMsg(),"SUCCESS");
} catch (IOException e) {
this.msg = "SUCCESS";
}
}
/**
* 获取成功状态结果,区分多语言(默认简体中文)
*
* @param language 语言类型,eg: en_us 表示美式英文
* @return
*/
public static ApiResultI18n success(String language) {
return success(null, language);
}
/**
* 获取成功状态结果,区分多语言(默认简体中文)
*
* @param data 返回数据
* @param language 语言类型,eg: en_us 表示美式英文
* @return
*/
public static ApiResultI18n success(Object data, String language) {
ApiResultI18n result = new ApiResultI18n(language);
result.setData(data);
return result;
}
/**
* 获取失败状态结果,区分多语言(默认简体中文)
*
* @param language 语言类型,eg: en_us 表示美式英文
* @return
*/
public static ApiResultI18n failure(String language) {
return failure(ResponseCodeI18n.FAIL.getCode(), ResponseCodeI18n.FAIL.getMsg(), null, language);
}
/**
* 获取失败结果,区分多语言(默认中文)
*
* @param responseCodeI18n 返回码
* @param language 语言类型
* @return
*/
public static ApiResultI18n failure(ResponseCodeI18n responseCodeI18n, String language) {
return failure(responseCodeI18n.getCode(), responseCodeI18n.getMsg(), null, language);
}
/**
* 获取失败状态结果,区分多语言(默认中文)
*
* @param code 返回状态码
* @param msg 错误信息
* @param language 语言类型,eg: en 表示英文
* @return
*/
public static ApiResultI18n failure(int code, String msg, String language) {
return failure(code ,msg, null, language);
}
/**
* 获取失败返回结果,区分多语言(默认中文)
*
* @param code 错误码
* @param msg 错误信息
* @param data 返回结果
* @param language 语言类型,eg: en 表示英文
* @return
*/
public static ApiResultI18n failure(int code, String msg, Object data, String language) {
ApiResultI18n result = new ApiResultI18n(language);
language = LanguageEnum.getLanguageType(language);
try {
msg = I18nMessageUtil.getMessage(language, msg, msg);
} catch (IOException e) {
msg = "Error";
}
result.setCode(code);
result.setMsg(msg);
result.setData(data);
if (data instanceof String) {
String m = (String) data;
if (!m.matches("^.*error$")) {
m += "error";
}
}
return result;
}
}
语言的数量根据项目的需要,这里只列举类中文和英文两种语言
中文:
../demo-web/src/main/resources/i18n/zh_cn.properties
英文:
../demo-web/src/main/resources/i18n/en_us.properties
中文i18n多语言配置文件信息
# language = zh_cn(简体中文)
# api response code
api.response.code.success=成功
api.response.code.fail=失败
api.response.code.paramError=参数错误
api.response.code.languageTypeError=语言类型错误
# 用户模块
# 帐号
api.response.code.user.accountNullError=帐号为空
api.response.code.user.accountFormatError=账号(格式)错误
api.response.code.user.accountNotExist=账号不存在
api.response.code.user.accountExist=账号已经被注册
# 密码
api.response.code.user.passwordNullError=密码为空
api.response.code.user.passwordFormatError=密码格式错误
api.response.code.user.passwordError=密码错误
# 未知异常
api.response.code.unknownError=未知异常
英文的配置和中文格式一致,这里就不再列出
项目配置了多语言之后,在接收参数的java bean 中需要添加语言字段,用于区分语言类型
语言类型字段属于一个公共字段,每一个接口都需要传递,建议写在一个基础公共类中,其他的参数接收类继承该类
参数接收基础 bean
../demo-model/src/main/java/com/ljq/demo/springboot/BaseBean.java
package com.ljq.demo.springboot;
import lombok.Data;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
/**
* @Description: 基础 bean
* @Author: junqiang.lu
* @Date: 2018/12/24
*/
@Data
public class BaseBean implements Serializable {
private static final long serialVersionUID = 6877955227522370690L;
/**
* 语言类型
*/
@Pattern(regexp = "^\\w{2,10}$", message = "api.response.code.languageTypeError")
private String language;
}
用户注册参数接收 bean
../demo-model/src/main/java/com/ljq/demo/springboot/vo/UserSignUpBean.java
package com.ljq.demo.springboot.vo;
import com.ljq.demo.springboot.BaseBean;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* @Description: 用户注册接收参数bean
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Data
public class UserSignUpBean extends BaseBean {
/**
* 账号
*/
@NotNull(message = "api.response.code.user.accountNullError")
@Pattern(regexp = "^\\S{5,50}$", message = "api.response.code.user.accountFormatError")
private String userName;
/**
* 密码
*/
@NotNull(message = "api.response.code.user.passwordNullError")
@Pattern(regexp = "^\\w{5,80}$", message = "api.response.code.user.passwordFormatError")
private String userPasscode;
}
DAO 层基本不涉及到多语言,这里便不再贴出相关代码
Service 层
用户模块Service 接口
../demo-service/src/main/java/com/ljq/demo/springboot/service/UserService.java
package com.ljq.demo.springboot.service;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import java.util.Map;
/**
* @Description: 用户模块业务
* @Author: junqiang.lu
* @Date: 2018/10/9
*/
public interface UserService {
/**
* 用户注册
*
* @param userSignUpBean 用户注册信息
* @return
*/
ApiResultI18n signUp(UserSignUpBean userSignUpBean) throws Exception;
}
用户模块 Service 实现类
../demo-service/src/main/java/com/ljq/demo/springboot/service/impl/UserServiceImpl.java
package com.ljq.demo.springboot.service.impl;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import com.ljq.demo.springboot.dao.user.UserDao;
import com.ljq.demo.springboot.entity.UserDO;
import com.ljq.demo.springboot.service.UserService;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @Description: 用户业务具体实现
* @Author: junqiang.lu
* @Date: 2018/10/9
*/
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 用户注册
*
* @param userSignUpBean 用户注册信息
* @return
*/
@Override
public ApiResultI18n signUp(UserSignUpBean userSignUpBean) throws Exception{
/**
* 参数校验顺序: 基本入参校验 --> 具体参数合法性校验(非数据库层校验) --> 数据库层参数校验
*/
/**
* 请求参数获取
*/
UserDO userParams = new UserDO();
BeanUtils.copyProperties(userSignUpBean, userParams);
/**
* 此处省略
* 参数合法性校验校验... ...
*/
// 注册帐号数据库层校验
int userCount = userDao.signUpCheck(userParams);
if (userCount > 0) {
return ApiResultI18n.failure(ResponseCodeI18n.ACCOUNT_EXIST, userSignUpBean.getLanguage());
}
/**
* 此处省略
* 用户注册相关操作... ...
*/
return ApiResultI18n.success(userSignUpBean.getLanguage());
}
}
用户模块 Controllerr 层
../demo-web/src/main/java/com/ljq/demo/springboot/web/controller/UserController.java
package com.ljq.demo.springboot.web.controller;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import com.ljq.demo.springboot.service.UserService;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 用户控制中心
* @Author: junqiang.lu
* @Date: 2018/10/9
*/
@RestController
@RequestMapping("api/user")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
/**
* 用户注册
*
* @param userSignUpBean 注册信息
* @return
*/
@RequestMapping(value = "signup", method = RequestMethod.POST)
public ApiResultI18n signUp(@RequestBody UserSignUpBean userSignUpBean){
ApiResultI18n apiResultI18n= null;
try {
apiResultI18n = userService.signUp(userSignUpBean);
} catch (Exception e) {
if (ParamsCheckException.class.isAssignableFrom(e.getClass())){
logger.error("注册失败,参数错误");
return apiResultI18n.failure(ResponseCodeI18n.PARAM_ERROR.getCode(), e.getMessage(),
userSignUpBean.getLanguage());
}
logger.error("注册失败,未知异常",e);
return apiResultI18n.failure(ResponseCodeI18n.UNKNOWN_ERROR, userSignUpBean.getLanguage());
}
return apiResultI18n;
}
}
本地测试接口地址
http://127.0.0.1:8088/api/user/signup
中文参数
{
"language" : "zh_cn",
"userName" : "tom",
"userPasscode" : "123456"
}
中文接口返回结果
{
"code": 1001,
"msg": "账号(格式)错误",
"data": null
}
中文接口请求日志
2019-01-28 11:30:46:390 [http-nio-8088-exec-3] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 66) -[AOP-LOG-START]
requestMark: cc866c67-5fdc-4adb-855e-a79d67d31344
requestIP: 127.0.0.1
contentType:application/json
requestUrl: http://127.0.0.1:8088/api/user/signup
requestMethod: POST
requestParams: {"language":"zh_cn","userName":"tom","userPasscode":"123456"}
targetClassAndMethod: com.ljq.demo.springboot.web.controller.UserController#signUp
2019-01-28 11:30:46:405 [http-nio-8088-exec-3] ERROR c.l.demo.springboot.web.controller.UserController(UserController.java 91) -注册失败,参数错误
2019-01-28 11:30:46:407 [http-nio-8088-exec-3] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 74) -[AOP-LOG-END]
ApiResultI18n(code=1001, msg=账号(格式)错误, data=null)
英文参数
{
"language" : "en_us",
"userName" : "tom",
"userPasscode" : "123456"
}
英文接口返回结果
{
"code": 1001,
"msg": "Account format error",
"data": null
}
英文接口请求日志
2019-01-28 11:32:51:672 [http-nio-8088-exec-4] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 66) -[AOP-LOG-START]
requestMark: 756575c3-471a-4873-8f93-4850b3353038
requestIP: 127.0.0.1
contentType:application/json
requestUrl: http://127.0.0.1:8088/api/user/signup
requestMethod: POST
requestParams: {"language":"en_us","userName":"tom","userPasscode":"123456"}
targetClassAndMethod: com.ljq.demo.springboot.web.controller.UserController#signUp
2019-01-28 11:32:51:692 [http-nio-8088-exec-4] ERROR c.l.demo.springboot.web.controller.UserController(UserController.java 91) -注册失败,参数错误
2019-01-28 11:32:51:693 [http-nio-8088-exec-4] INFO com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 74) -[AOP-LOG-END]
ApiResultI18n(code=1001, msg=Account format error, data=null)
至此,一个 Spring boot RESTful 风格的多语言国际化 API 接口框架已经搭建完成
Spring 读取i18n国际化资源文件的工具类
自己动手在Spring-Boot上加强国际化功能
使rest的消息和内容国际化
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.