消息提示国际化
获取Locale
实现国际化的第一步是获取到用户的Locale
。在Web应用程序中,HTTP规范规定了浏览器会在请求中携带Accept-Language
头,用来指示用户浏览器设定的语言顺序,如:
Accept-Language: zh-CN,zh;q=0.8,en;q=0.2
上述HTTP请求头表示优先选择简体中文,其次选择中文,最后选择英文。q
表示权重,解析后我们可获得一个根据优先级排序的语言列表,把它转换为Java的Locale
,即获得了用户的Locale
。大多数框架通常只返回权重最高的Locale
。
Spring MVC通过LocaleResolver
来自动从HttpServletRequest
中获取Locale
。LocaleResolver
的四种实现:
AcceptHeaderLocaleResolver:默认的区域解析器实现,不可手动配置,通过请求头Accept-Language
来获取Locale信息。这里需要注意的是HttpServletRequest
将Accept-Language
头中的语言字符串转为Locale
对象时只可使用IETF语言标签(即en-US
),而无法使用ISO语言标签(即en_US
)。
CookieLocaleResolver:通过客户端的Cookie
获取相应的Locale
,Cookie为空时再从Accept-Language
头中获取。目前最为常用的实现方式。
SessionLocaleResolver:通过Session获取相应的Locale
,在目前多为分布式系统架构的情况下使用太过局限,不推荐使用。
FixedLocaleResolver:固定的区域解析器,总是返回一个默认的locale
,不指定默认值时为JVM的默认locale
,不支持setLocale
方法,不能被改变。
在这里我们还是使用最为常用的CookieLocaleResolver
来获取Locale
。
@Bean
public LocaleResolver localeResolver() {
LocaleResolver localeResolver = new CookieLocaleResolver();
return localeResolver;
}
当用户第一次访问网站时,CookieLocaleResolver
只能从Accept-Language
中获取Locale再设置到Cookie
中,下次访问就会直接使用Cookie
的设置。CookieLocaleResolver
设置的cookie
默认有效期为会话结束,我们可以自定义子类来重新设置默认的属性。
public class MyCookieLocaleResolver extends CookieLocaleResolver {
//重写构造方法,改变cookie信息
public MyCookieLocaleResolver(){
this.setCookieName("Locale");
//cookie有效期30天
this.setCookieMaxAge(30*24*60*60);
}
}
如果用户想通过请求接口来切换Locale
,还可以配置区域变更拦截器LocaleChangeInterceptor
,LocaleChangeInterceptor
会配合当前使用的LocaleResolver
,使用其setLocale()
方法来设置新的Locale
。
@Bean
public WebMvcConfigurer i18nInterceptor() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor i18nInterceptor = new LocaleChangeInterceptor();
// 设置请求参数名,默认为locale
i18nInterceptor.setParamName("lang");
registry.addInterceptor(i18nInterceptor);
}
};
}
设置拦截器后即可通过URL?lang=zh_CN
方式来切换语言信息,切换后的语言信息会直接存入到Cookie
中。
创建资源文件
在resource下新建i18n文件目录,添加多语言配置文件。默认语言,文件名必须使用message.properties,其他语言使用message加Locale名的形式命名。
messages_en_US.properties
user.name=zhangsan
messages_zh_CN.properties
user.name=张三
查找出代码中所有写死的中文消息提示,建立多语言配置
匹配双字节字符(包括汉字在内):[^\x00-\xff]+
创建MessageSource
创建一个Spring提供的MessageSource
实例,它自动读取所有的.properties
文件。Spring容器可以创建不止一个MessageSource实例,我们可以把自己创建的MessageSource命名为i18nMessageSource,防止和其他实例冲突。
@Bean("i18nMessageSource")
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
提供一个统一的工具类来获取多语言配置信息
@UtilityClass
public class MsgUtils {
public String getMessage(String code) {
MessageSource messageSource = SpringContextHolder.getBean("i18nMessageSource");
return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
}
public String getMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("i18nMessageSource");
return messageSource.getMessage(code, objects, LocaleContextHolder.getLocale());
}
}
字典国际化
添加Locale字段
Web系统中消息提示的内容改动并不频繁,可以直接使用资源文件来配置,但系统字典的信息多为用户自定义,会频繁修改其内容,在字典国际化的实现中再去使用资源文件配置就不太合适了。通过添加locale字段区分不同语言的字典字段,字典表设计如下:
// 字典类型表
CREATE TABLE `sys_dict` (
`id` int NOT NULL AUTO_INCREMENT,
`type` varchar(100) NOT NULL COMMENT '类型',
`description` varchar(100) DEFAULT NULL COMMENT '描述'
);
// 字典项明细表
CREATE TABLE `sys_dict_item` (
`id` int NOT NULL AUTO_INCREMENT,
`dict_id` int NOT NULL COMMENT '字典ID',
`type` varchar(100) NOT NULL COMMENT '字典类型',
`locale` varchar(10) NOT NULL COMMENT '语言区域',
`key` varchar(100) NOT NULL,
`value` varchar(100) NOT NULL
);
动态缓存获取
由于字典信息会在系统中频繁使用,不可能每次使用都去重新查询数据库,将字典信息存储到缓存中可以有效提高效率,使用缓存注解在查询列表请求的同时缓存列表信息
@GetMapping("/type/{type}")
@Cacheable(value = "dict_details", key="#type")
public Object list(@PathVariable String type){
// 查询数据库获取字典类型列表信息
return list;
}
但是这样只能缓存指定的字典类型,还需要实现其多语言的切换,重写Spring中KeyGenerator的接口实现,在缓存key值的生成过程中带上Locale信息
public class LocalizedGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
String key = params[0] + "_" + LocaleContextHolder.getLocale();
return key;
}
}
在@Cacheable注解中指定keyGenerator
@Cacheable(value = "dict_details", keyGenerator = "localizedGenerator")
操作日志国际化
多语言配置
操作日志的国际化同消息提示的国际化,可以将日志内容的模板添加到资源文件中,参数可动态传入。
log_en_US.properties
log.user.login=user {0} login success
log_zh_CN.properties
log.user.login=用户{0}登录成功
定义对应的logMessageSource实例
@Bean("logMessageSource")
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:log/log");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
获取多语言操作日志信息
MessageSource messageSource = SpringContextHolder.getBean("i18nMessageSource");
String logContent = messageSource.getMessage("log.user.login", "test", LocaleContextHolder.getLocale());
System.out.println(logContent);
日志表仅需要存储多语言资源文件中的key值和动态传入的参数,查询的时候再通过messageSource来获取对应的日志消息。动态参数以json格式存储在mysql数据库,方便使用。
日志表设计:
CREATE TABLE `sys_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`template` varchar(255) DEFAULT NULL COMMENT '日志模板',
`template_params` json DEFAULT NULL COMMENT '模板参数',
`operate_time` datetime DEFAULT NULL COMMENT '操作时间',
`operate_by` varchar(64) DEFAULT NULL COMMENT '操作人'
);
mybatisplus处理数据库json数据:
@Data
@TableName(value = "sys_log",autoResultMap = true)
public class SysLog extends Model<SysLog> {
@TableField(typeHandler = JacksonTypeHandler.class)
private JSONArray templateParams;
}
自定义日志注解
现在完成了操作日志的多语言信息的获取,在项目中每个业务逻辑处理的过程中都需要记录操作日志,需要使用AOP切面技术将日志记录操作统一处理。
自定义日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
// 日志模板
String template();
// 模板参数
String[] params() default {};
}
自定义日志切面,对日志模板参数加入SpEL支持
@Aspect
public class SysLogAspect {
private static final DefaultParameterNameDiscoverer DEFAULT_PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext();
private static final ThreadLocal<StandardEvaluationContext> StandardEvaluationContextThreadLocal = new ThreadLocal<>();
@Around("@annotation(sysLog)")
@SneakyThrows
public Object around(ProceedingJoinPoint point, com.goodview.ipp.common.log.annotation.SysLog sysLog) {
// 模板参数数组
String[] templateParams = sysLog.params();
if(templateParams.length > 0) {
// 获取切点参数列表
Map<String, Object> methodParams = getMethodParams(point);
for (int i = 0; i < templateParams.length; i++) {
// 解析每个模板参数的值
templateParams[i] = parseExpression(templateParams[i], methodParams);
}
}
// 填充日志实体
SysLog logVo = new SysLong;
logVo.setTemplate(sysLog.value());
// 以json格式保存模板参数
if(templateParams.length > 0) {
JSONArray json = new JSONArray();
for(String str : templateParams) {
json.add(str);
}
logVo.setTemplateParams(json);
}
logVo.setOperateBy(user.getUsername());
logVo.setOperateTime(LocalDateTime.now());
// 发送异步日志事件
Object obj;
try {
obj = point.proceed();
} catch (Exception e) {
// TODO 异常处理
throw e;
} finally {
SpringContextHolder.publishEvent(new SysLogEvent(logVo));
}
return obj;
}
private Map<String, Object> getMethodParams(ProceedingJoinPoint point){
MethodSignature signature = (MethodSignature) point.getSignature();
String methodName = point.getSignature().getName();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Method method;
try {
method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ex) {
ex.printStackTrace(System.out);
return null;
}
Object[] args = point.getArgs();
String[] parameterNames = DEFAULT_PARAMETER_NAME_DISCOVERER.getParameterNames(method);
Map<String, Object> methodParams = new HashMap<>();
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
methodParams.put(parameterNames[i], args[i]);
}
}
return methodParams;
}
private String parseExpression(String template, Map<String, Object> params) {
// 将ioc容器设置到上下文中
ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();
// 线程初始化StandardEvaluationContext
StandardEvaluationContext standardEvaluationContext = StandardEvaluationContextThreadLocal.get();
if(standardEvaluationContext == null){
standardEvaluationContext = new StandardEvaluationContext(applicationContext);
standardEvaluationContext.addPropertyAccessor(new BeanFactoryAccessor());
standardEvaluationContext.setBeanResolver(new BeanFactoryResolver(applicationContext));
StandardEvaluationContextThreadLocal.set(standardEvaluationContext);
}
// 将自定义参数添加到上下文
standardEvaluationContext.setVariables(params);
// 解析表达式
Expression expression = EXPRESSION_PARSER.parseExpression(template, TEMPLATE_PARSER_CONTEXT);
return expression.getValue(standardEvaluationContext, String.class);
}
}
使用方式
// 纯日志消息
@SysLog(template = "log.user.login")
// 获取方法参数值
@SysLog(template = "log.user.login", params = {"#{username}"})
// 获取方法参数对象的属性的值
@SysLog(template = "log.user.login", params = {"#{user.username}"})
// 通过方法参数查询数据
@SysLog(template = "log.user.login", params = {"#{@userService.getUserName(#id)}"})