Javaweb国际化解决方案
创始人
2025-05-28 20:46:26
0

消息提示国际化

获取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中获取LocaleLocaleResolver的四种实现:
AcceptHeaderLocaleResolver:默认的区域解析器实现,不可手动配置,通过请求头Accept-Language来获取Locale信息。这里需要注意的是HttpServletRequestAccept-Language头中的语言字符串转为Locale对象时只可使用IETF语言标签(即en-US),而无法使用ISO语言标签(即en_US)。
CookieLocaleResolver:通过客户端的Cookie获取相应的Locale,Cookie为空时再从Accept-Language头中获取。目前最为常用的实现方式。
SessionLocaleResolver:通过Session获取相应的Locale,在目前多为分布式系统架构的情况下使用太过局限,不推荐使用。
FixedLocaleResolver:固定的区域解析器,总是返回一个默认的locale,不指定默认值时为JVM的默认locale,不支持setLocale方法,不能被改变。

在这里我们还是使用最为常用的CookieLocaleResolver来获取Locale

	@Beanpublic 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,还可以配置区域变更拦截器LocaleChangeInterceptorLocaleChangeInterceptor会配合当前使用的LocaleResolver,使用其setLocale()方法来设置新的Locale

@Bean
public WebMvcConfigurer i18nInterceptor() {return new WebMvcConfigurer() {@Overridepublic void addInterceptors(InterceptorRegistry registry) {LocaleChangeInterceptor i18nInterceptor = new LocaleChangeInterceptor();// 设置请求参数名,默认为localei18nInterceptor.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 {@Overridepublic 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 {@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 StandardEvaluationContextThreadLocal = new ThreadLocal<>();@Around("@annotation(sysLog)")@SneakyThrowspublic Object around(ProceedingJoinPoint point, com.goodview.ipp.common.log.annotation.SysLog sysLog) {// 模板参数数组String[] templateParams = sysLog.params();if(templateParams.length > 0) {// 获取切点参数列表Map 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 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 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 params) {// 将ioc容器设置到上下文中ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();// 线程初始化StandardEvaluationContextStandardEvaluationContext 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)}"})

相关内容

热门资讯

安卓系统的经典铃声,唤醒回忆的... 你有没有发现,手机里那些熟悉的铃声,有时候就像老朋友一样,陪伴着我们度过了无数个日日夜夜?今天,就让...
鸿蒙系统还是安卓系统号,系统之... 你有没有想过,手机里的操作系统就像是我们的大脑,它决定了我们手机能做什么,不能做什么。现在,就让我们...
安卓系统装贝达,安卓系统下的贝... 你有没有想过,你的安卓手机装上贝达系统后,会有怎样的奇妙体验呢?想象你的手机瞬间变身,变得流畅无比,...
安卓系统沃尔沃音响设置,轻松享... 你有没有发现,自从你的安卓手机和沃尔沃音响完美结合后,开车时的音乐体验简直就像是在音乐厅里一样?没错...
米10系统基于安卓,基于安卓的... 你知道吗?最近手机圈里可是热闹非凡呢!小米10这款手机,自从发布以来就吸引了无数人的目光。而它所搭载...
命令安卓系统怎么卸载,安卓系统... 手机里装了太多不用的应用,是不是感觉手机都快要爆炸了?别急,今天就来教你怎么轻松卸载安卓系统中的应用...
安卓系统安装小学教材,安卓系统... 你有没有想过,手机里的安卓系统竟然能装上小学教材呢?没错,你没听错!在这个信息爆炸的时代,科技的发展...
华为安卓系统锁住了,揭秘锁屏背... 最近是不是发现你的华为手机有点儿“顽皮”了?它突然间变得神秘起来,屏幕上那个熟悉的安卓系统仿佛被施了...
安卓电脑改苹果系统,跨越平台的... 你有没有想过,把你的安卓电脑改头换面,变成一个优雅的苹果系统使用者呢?想象那流畅的界面,那独特的触控...
安卓系统怎么按后台,并在任务完... 你有没有遇到过这种情况:手机屏幕一黑,安卓系统就自动进入后台了?是不是觉得有点小郁闷,想要手动切换回...
2021年安卓系统ui,202... 你有没有发现,手机界面最近好像换了个模样?没错,2021年的安卓系统UI可是来了一场大变身呢!今天,...
安卓系统程序编写软件,打造个性... 你有没有想过,手机里的那些神奇应用是怎么诞生的呢?没错,就是那些让你在闲暇时光刷刷视频、在通勤路上玩...
自动开机安卓系统,智能生活新篇... 你有没有想过,当你的安卓手机在清晨的第一缕阳光照耀下自动开机,那种轻松自在的感觉?想象不用再手动解锁...
真我平板x安卓系统,畅享智能生... 亲爱的读者们,你是否也在寻找一款既能满足你对平板电脑的期待,又能让你畅享安卓系统带来的无限乐趣的设备...
恒星安卓系统官网,引领未来智能... 亲爱的读者们,你是否曾好奇过那些闪耀在夜空中的星星,它们是如何在浩瀚的宇宙中熠熠生辉的呢?今天,我要...
u8安卓系统,功能与特色深度解... 你知道吗?在手机操作系统界,有一个小家伙可是相当受欢迎的,它就是U8安卓系统。今天,就让我带你来一探...
花椒安卓系统美颜功能,打造完美... 你有没有发现,现在拍照已经不仅仅是记录生活的工具了,它更是一种艺术创作呢!而在这其中,花椒安卓系统的...
戴尔平板升级安卓系统,畅享安卓... 你有没有发现,戴尔平板最近好像悄悄地来了一次大变身?没错,就是那个我们熟悉的戴尔平板,它现在竟然可以...
安卓助手怎么升级系统,畅享最新... 亲爱的安卓用户们,你是否也和我一样,对安卓系统的升级充满了期待和好奇呢?每次系统升级,都仿佛是给我们...
国产安卓系统的发展,国产安卓系... 你知道吗?在我国科技飞速发展的今天,国产安卓系统可是越来越受到大家的关注呢!它就像一颗冉冉升起的新星...