Springboot国际化方案 (非MessageSource)
一、实现思路(响应切面处理器+全局异常处理器)
1.定义响应切面处理器
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return isSupport(methodParameter);
}
@Override
public R beforeBodyWrite(R r, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
//手动翻译文本
String response_msg = TranslateExecuteWrapper.translateText("response_msg", r.getMsg());
r.setMsg(response_msg);
//手动翻译对象
TranslateExecuteWrapper.translateObject(r.getData());
return r;
}
/**
* 仅支持返回值类型为R 并且排除异常处理器类型 防止重复处理异常信息
* @param methodParameter
* @return
*/
private boolean isSupport(MethodParameter methodParameter) {
return methodParameter.getMethod().getAnnotation(ExceptionHandler.class) == null && methodParameter.getParameterType().isAssignableFrom(R.class);
}
2.全局异常解析器
private String executeTranslate(String code, Integer statusCode, String exceptionMsg) {
return TranslateExecuteWrapper.translateText(exceptionCode, code, prodI18nErrorParam(statusCode, exceptionMsg));
}
private Map<String, String> prodI18nErrorParam(Integer code, String exceptionMsg) {
Map<String, String> params = new HashMap<>();
params.put("code", String.valueOf(code));
params.put("msg", exceptionMsg);
return params;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<String> handleGlobalException(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
globalExceptionHandler.handle(e);
// 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
String errorMsg = GlobalConstants.ENV_PROD.equals(profile) ? PROD_ERR_MSG
: (e instanceof NullPointerException ? NLP_MSG : e.getLocalizedMessage());
String exceptionMsg = executeTranslate("global.exception", SystemResultCode.BAD_REQUEST.getCode(), errorMsg);
return R.failed(SystemResultCode.SERVER_ERROR, exceptionMsg);
}
二、主要工具类
TranslateExecuteWrapper
里面包含文本翻译 与对象翻译不指定语言 默认使用上下文语言环境
文本翻译
//翻译文本指定业务码 key
public static String translateText(String businessCode, String code);
//翻译文本指定业务码 key 语言环境
public static String translateText(String businessCode, String code, String language)
//翻译文本指定业务码 key 模板参数
public static String translateText(String businessCode, String code, Map<String, String> params)
//翻译文本指定业务码 key 语言环境 参数
public static String translateText(String businessCode, String code, String language, Map<String, String> params)
对象翻译
//翻译对象 传递对象
public static void translateObject(Object source)
//翻译对象 传递对象 与上下文参数
public static void translateObject(Object source, Map<String, String> params)
三、结果展示
实体类
@I18nField(businessCode = "dict")
private String title;
@I18nField(businessCode = "dict", rangeValue = { "是", "否" })
private String status;
@I18nField(businessCode = "dict", rangeValue = { "开", "关" }, defaultValue = "不开")
private String open;
/**
* 字典不存在 会放入空标记到缓存
*/
@I18nField(businessCode = "dict")
private String nullValue;
/**
* 若list内为基础类型 或String 则跳过
*/
private List<String> stringList;
/**
* 递归翻译
*/
private List<I18nUser> users;
/**
* 递归翻译
*/
private Set<I18nUser> userSet;
赋值实体
I18nUser i18nUser = new I18nUser().setTitle("测试").setOpen("开").setStatus("是");
I18nUser i18nUserListElement = new I18nUser().setTitle("测试").setOpen("关").setStatus("否");
i18nUser.setUsers(ListUtil.of(i18nUserListElement));
I18nUser clone = ObjectUtil.clone(i18nUserListElement);
HashSet<I18nUser> objects = new HashSet<>();
objects.add(clone);
i18nUser.setUserSet(objects);
List<String> of = ListUtil.of("测试", "开");
i18nUser.setStringList(of);
i18nUser.setNullValue("not_exist");
测试中文
请求头指定 lang: zh_CN
{
"code": 200,
"message": "成功",
"data": {
"title": "测试",
"status": "是",
"open": "开",
"nullValue": "not_exist",
"stringList": [
"测试",
"开"
],
"users": [
{
"title": "测试",
"status": "否",
"open": "关",
"nullValue": "",
"stringList": [],
"users": [],
"userSet": []
}
],
"userSet": [
{
"title": "测试",
"status": "否",
"open": "关",
"nullValue": "",
"stringList": [],
"users": [],
"userSet": []
}
]
}
}
测试英文
请求头指定 lang: en_US
{
"code": 200,
"message": "Success",
"data": {
"title": "Test",
"status": "Yes",
"open": "Open",
"nullValue": "not_exist",
"stringList": [
"测试",
"开"
],
"users": [
{
"title": "Test",
"status": "No",
"open": "Close",
"nullValue": "",
"stringList": [],
"users": [],
"userSet": []
}
],
"userSet": [
{
"title": "Test",
"status": "No",
"open": "Close",
"nullValue": "",
"stringList": [],
"users": [],
"userSet": []
}
]
}
}
四、快速上手
一、建表语句
CREATE TABLE `i18n_data` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`system_name` varchar(20) DEFAULT NULL COMMENT '系统名称',
`business_code` varchar(20) DEFAULT NULL COMMENT '业务码',
`code` varchar(100) DEFAULT NULL COMMENT 'code',
`language` varchar(20) DEFAULT NULL COMMENT '语言环境 zh_CN',
`value` varchar(50) DEFAULT NULL COMMENT '值',
`type` tinyint(4) DEFAULT NULL COMMENT '类型 1 明文 2模板',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_uni_code_language` (`code`,`language`),
UNIQUE KEY `idx_uni_sys_bs_code_language` (`system_name`,`business_code`,`code`,`language`)
) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8 COMMENT='i18n数据表'
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (1, 'test-item', 'dict', '是', 'zh_CN', '是', 1, '2021-04-02 09:31:36', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (2, 'test-item', 'dict', '是', 'en_US', 'Yes', 1, '2021-04-02 09:32:01', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (3, 'test-item', 'dict', '否', 'zh_CN', '否', 1, '2021-04-02 09:33:55', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (4, 'test-item', 'dict', '否', 'en_US', 'No', 1, '2021-04-02 09:34:18', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (5, 'test-item', 'dict', '开', 'zh_CN', '开', 1, '2021-04-02 09:33:55', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (6, 'test-item', 'dict', '开', 'en_US', 'Open', 1, '2021-04-02 09:34:18', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (7, 'test-item', 'dict', '关', 'zh_CN', '关', 1, '2021-04-02 09:33:55', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (8, 'test-item', 'dict', '关', 'en_US', 'Close', 1, '2021-04-02 09:34:18', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (9, 'test-item', 'dict', '测试', 'zh_CN', '测试', 1, '2021-04-02 09:33:55', NULL);
INSERT INTO `i18n_data`(`id`, `system_name`, `business_code`, `code`, `language`, `value`, `type`, `create_time`, `update_time`) VALUES (10, 'test-item', 'dict', '测试', 'en_US', 'Test', 1, '2021-04-02 09:34:18', NULL);
二、引入relaxed-spring-boot-starter-i18n
坐标
<dependency>
<groupId>com.lovecyy</groupId>
<artifactId>relaxed-spring-boot-starter-i18n</artifactId>
<version>${revision}</version>
</dependency>
三、配置application.yml
spring:
application:
name: relaxed-samples-i18n
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://relaxed-mysql:3306/ballcat?serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false&nullNamePatternMatchesAll=true&zeroDateTimeBehavior=CONVERT_TO_NULL&tinyInt1isBit=false&autoReconnect=true&useSSL=false&pinGlobalTxToPhysicalConnection=true
username: root
password: 123456
redis:
host: relaxed-redis
password: '123456'
port: 8007
# mybatis-plus相关配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*Mapper.xml
global-config:
banner: false
relaxed:
i18n:
#系统名称
systemName: test-item
#缓存空值标记 防止缓存穿透
nullValue: N_V
#执行器 默认使用simple 推荐cache
executor: cache
#key 生成器分隔符
generate:
delimiter: ':'
#缓存配置
cache:
#默认local
type: redis
#过期时间 local 默认无限 cache 小于0 则永不过期
expire: -1
四、实体类
1.R 通用返回
@Data
public class R {
private Integer code;
private String msg;
private Object data;
public R() {
}
public R(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public R(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static R ok() {
return new R(200, "Success");
}
public static R ok(Object data) {
return new R(200, "Success", data);
}
public static R fail(String msg) {
return new R(500, msg, null);
}
}
2.I18nUser
@Accessors(chain = true)
@Data
public class I18nUser implements Serializable {
@I18nField(businessCode = "dict")
private String title;
@I18nField(businessCode = "dict", rangeValue = { "是", "否" })
private String status;
@I18nField(businessCode = "dict", rangeValue = { "开", "关" }, defaultValue = "不开")
private String open;
/**
* 字典不存在 会放入空标记到缓存
*/
@I18nField(businessCode = "dict")
private String nullValue;
/**
* 若list内为基础类型 或String 则跳过
*/
private List<String> stringList;
/**
* 递归翻译
*/
private List<I18nUser> users;
/**
* 递归翻译
*/
private Set<I18nUser> userSet;
}
五、配置区域解析器
请求头要指定语言 lang: zh_CN
/**
* 国际化配置
*
* @author Yakir
*/
@Configuration
public class LocaleConfig {
/**
* 区域解析器
*/
@Bean
public LocaleResolver localeResolver() {
MyLocaleResolver localeResolver = new MyLocaleResolver();
return localeResolver;
}
class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = request.getHeader("lang");
if (StrUtil.isEmpty(language)) {
// 路径上没有国际化语言参数,采用默认的(从请求头中获取)
return request.getLocale();
}
else {
// 格式语言_国家 en_US
return StringUtils.parseLocale(language);
}
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Locale locale) {
}
}
}
六、配置I18nResponseAdvice
/**
* 只针对R的msg进行国际化处理
*
* @author Yakir
*/
@RequiredArgsConstructor
@RestControllerAdvice
public class I18nResponseAdvice implements ResponseBodyAdvice<R> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return isSupport(methodParameter);
}
@Override
public R beforeBodyWrite(R r, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
String response_msg = TranslateExecuteWrapper.translateText("response_msg", r.getMsg());
r.setMsg(response_msg);
TranslateExecuteWrapper.translateObject(r.getData());
return r;
}
/**
* 仅支持返回值类型为R 与不为错误处理器返回过来的类型
* @param methodParameter
* @return
*/
private boolean isSupport(MethodParameter methodParameter) {
return methodParameter.getMethod().getAnnotation(ExceptionHandler.class) == null && methodParameter.getParameterType().isAssignableFrom(R.class);
}
}
七、实现I18nDataProvider
数据提供接口
@Override
public I18nItem selectOne(String systemName, String businessCode, String code, String language) {
I18nData i18nData = i18nDataService.getOne(Wrappers.lambdaQuery(I18nData.class)
.eq(I18nData::getSystemName, systemName).eq(I18nData::getBusinessCode, businessCode)
.eq(I18nData::getCode, code).eq(I18nData::getLanguage, language));
return convertToI18nItem(i18nData);
}
private I18nItem convertToI18nItem(I18nData i18nData) {
if (i18nData == null) {
return null;
}
return new I18nItem().setSystemName(i18nData.getSystemName()).setBusinessCode(i18nData.getBusinessCode())
.setCode(i18nData.getCode()).setLanguage(i18nData.getLanguage()).setType(i18nData.getType())
.setValue(i18nData.getValue());
}
八、配置全局异常解析器
主要负责对异常信息进行国际化
private String executeTranslate(String code, Integer statusCode, String exceptionMsg) {
return TranslateExecuteWrapper.translateText(exceptionCode, code, prodI18nErrorParam(statusCode, exceptionMsg));
}
private Map<String, String> prodI18nErrorParam(Integer code, String exceptionMsg) {
Map<String, String> params = new HashMap<>();
params.put("code", String.valueOf(code));
params.put("msg", exceptionMsg);
return params;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<String> handleGlobalException(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
globalExceptionHandler.handle(e);
// 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
String errorMsg = GlobalConstants.ENV_PROD.equals(profile) ? PROD_ERR_MSG
: (e instanceof NullPointerException ? NLP_MSG : e.getLocalizedMessage());
String exceptionMsg = executeTranslate("global.exception", SystemResultCode.BAD_REQUEST.getCode(), errorMsg);
return R.failed(SystemResultCode.SERVER_ERROR, exceptionMsg);
}
九、测试国际化
@GetMapping("/json/serialize")
public R testWebContentTJsonSerialize() {
I18nUser i18nUser = new I18nUser().setTitle("测试").setOpen("开").setStatus("是");
I18nUser i18nUserListElement = new I18nUser().setTitle("测试").setOpen("关").setStatus("否");
i18nUser.setUsers(ListUtil.of(i18nUserListElement));
I18nUser clone = ObjectUtil.clone(i18nUserListElement);
HashSet<I18nUser> objects = new HashSet<>();
objects.add(clone);
i18nUser.setUserSet(objects);
List<String> of = ListUtil.of("测试", "开");
i18nUser.setStringList(of);
i18nUser.setNullValue("not_exist");
return R.ok(i18nUser);
}
@GetMapping("/json/exception")
public R testException() {
throw new RuntimeException( "mysql connection faild");
}
附: demo地址:https://gitee.com/TomSale/relaxed.git