参考资料
- springboot项目实现数据库读取国际化配置
- 路人甲博客-国际化详解
- Spring源码分析-MessageSource
- Spring Boot 架构中的国际化支持实践—— Spring Boot 全球化解决方案
目录
一. 前期准备
1.1 国际化信息
⏹messages_zh.properties
1001E=请输入{msgArgs}。
1005E=请输入半角数字。
⏹messages_ja.properties
1001E={msgArgs}を入力してください。
1005E=半角数字を入力してください。
⏹数据库
1.2 自定义校验注解
⏹校验是否为空
import javax.validation.Constraint;
import javax.validation.constraints.NotEmpty;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.*;
@Documented
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@NotEmpty
@ReportAsSingleViolation
public @interface ValidateNotEmpty {
String msgArgs() default "";
String message() default "{1001E}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
⏹校验是否为半角数字
import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
@Constraint(validatedBy = {})
@ConstraintComposition(CompositionType.AND)
@NotEmpty
@Pattern(regexp = "[0-9]*")
@ReportAsSingleViolation
// 标记该注解是否可重复使用
@Repeatable(ValidateHalfNumeric.List.class)
public @interface ValidateHalfNumeric {
String message() default "{1005E}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ FIELD })
@Retention(RUNTIME)
@Documented
public @interface List {
ValidateHalfNumeric[] value();
}
}
1.3 待校验form
import lombok.Data;
@Data
public class Test9Form {
@ValidateNotEmpty(msgArgs = "id")
private String id;
@ValidateHalfNumeric
private String age;
}
1.4 数据库查询接口
import java.util.List;
import com.example.jmw.entity.I18MessageEnttiy;
public interface I18nMessageMapper {
// 查询所有的国际化消息,封装到自定义的I18MessageEnttiy中
List<I18MessageEnttiy> getAllLocaleMessage();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.jmw.mapper.I18nMessageMapper">
<select id="getAllLocaleMessage" resultType="com.example.jmw.entity.I18MessageEnttiy">
SELECT
code
, locale
, item
FROM
i18message
</select>
</mapper>
1.5 前台html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script type="text/javascript" th:src="@{/js/common/common.js}"></script>
<title>test9页面</title>
</head>
<body>
<button id="btn1">发送请求,进行校验</button>
<!-- 国际化语言展示区域 -->
<div>[[#{M004}]]</div>
<div>[[#{M002}]]</div>
<div>[[#{M003}]]</div>
<hr>
<button id="reloadMessage">重新加载Message</button>
</body>
<script>
$("#btn1").click(() => {
const param = {
id: null,
age: "测试年龄"
};
// 读取url中的参数,指定当前页面的校验语言
const languageParam = new URL(window.location.href).searchParams.get("language");
const url = `http://localhost:8080/test9/validate?language=${languageParam}`;
doAjax(url, param, function(data) { });
});
// 当数据库修改数据之后,需要手动触发 重新加载国际化消息方法
$("#reloadMessage").click(() => {
const url = `http://localhost:8080/test9/reloadMessage`;
doAjax(url, null, function(data) {});
});
</script>
</html>
二. 国际化相关配置
2.1 指定i18n国际化文件路径
spring:
messages:
basename: i18n/messages
encoding: UTF-8
2.2 自定义MessageSource类整合国际化消息
要点
ResourceBundle.getBundle()
读取国际化文件消息Collectors.groupingBy()
分组Collectors.toMap()
收集为MapLocaleContextHolder.getLocale()
获取设置的当前地区Locale
import com.example.jmw.entity.I18MessageEnttiy;
import com.example.jmw.mapper.I18nMessageMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
// @Component("messageSource"): 也可以在此处指明bean的名称为 messageSource
public class CustomMessageSource extends AbstractMessageSource implements InitializingBean {
// 这个是用来缓存数据库中获取到的配置的 数据库配置更改的时候可以调用reload方法重新加载
private static final Map<String, Map<String, String>> LOCAL_CACHE = new ConcurrentHashMap<>();
// 注入查询接口对象
@Resource
private I18nMessageMapper i18nMessageMapper;
// 程序启动之后,会自动加载
@Override
public void afterPropertiesSet() {
this.reload();
}
// 重新加载消息到该类的Map缓存中
public void reload() {
// 清除该类的缓存
LOCAL_CACHE.clear();
// 加载所有的国际化资源
Map<String, Map<String, String>> localeMsgMap = this.loadAllMessageResources();
LOCAL_CACHE.putAll(localeMsgMap);
}
// 加载所有的国际化消息资源
private Map<String, Map<String, String>> loadAllMessageResources() {
// 从数据库中查询所有的国际化资源
List<I18MessageEnttiy> allLocaleMessage = i18nMessageMapper.getAllLocaleMessage();
if (ObjectUtils.isEmpty(allLocaleMessage)) {
allLocaleMessage = new ArrayList<>();
}
// 将查询到的国际化资源转换为 Map<地区码, Map<code, 信息>> 的数据格式
Map<String, Map<String, String>> localeMsgMap = allLocaleMessage
// stream流
.stream()
// 分组
.collect(Collectors.groupingBy(
// 根据国家地区分组
I18MessageEnttiy::getLocale,
// 收集为Map,key为code,value为信息
Collectors.toMap(
I18MessageEnttiy::getCode
, I18MessageEnttiy::getItem
)
));
// 获取国家地区List
List<Locale> localeList = localeMsgMap.keySet().stream().map(Locale::new).collect(Collectors.toList());
for (Locale locale : localeList) {
// 按照国家地区来读取本地的国际化资源文件,我们的国际化资源文件放在i18n文件夹之下
ResourceBundle resourceBundle = ResourceBundle.getBundle("i18n/messages", locale);
// 获取国际化资源文件中的key和value
Set<String> keySet = resourceBundle.keySet();
// 将 code=信息 格式的数据收集为 Map<code,信息> 的格式
Map<String, String> msgFromFileMap = keySet.stream()
.collect(
Collectors.toMap(
Function.identity(),
resourceBundle::getString
)
);
// 将本地的国际化信息和数据库中的国际化信息合并
Map<String, String> localeFileMsgMap = localeMsgMap.get(locale.getLanguage());
localeFileMsgMap.putAll(msgFromFileMap);
localeMsgMap.put(locale.getLanguage(), localeFileMsgMap);
}
return localeMsgMap;
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
String msg = this.getSourceFromCacheMap(code, locale);
return new MessageFormat(msg, locale);
}
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
return this.getSourceFromCacheMap(code, locale);
}
// 缓存Map中加载国际化资源
private String getSourceFromCacheMap(String code, Locale locale) {
String language = ObjectUtils.isEmpty(locale)
? LocaleContextHolder.getLocale().getLanguage() : locale.getLanguage();
// 获取缓存中对应语言的所有数据项
Map<String, String> propMap = LOCAL_CACHE.get(language);
if (!ObjectUtils.isEmpty(propMap) && propMap.containsKey(code)) {
// 如果对应语言中能匹配到数据项,那么直接返回
return propMap.get(code);
}
// 如果找不到国际化消息,就直接返回code
return code;
}
}
2.3 国际化配置类
要点
- 自定义MessageSource类要交给Spring管理,bean名称一定要叫
messageSource
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
@Configuration
public class InternationalConfig implements WebMvcConfigurer {
// 默认解析器,用来设置当前会话默认的国际化语言
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
// 指定当前项目的默认语言是中文
sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return sessionLocaleResolver;
}
// 默认拦截器,用来指定切换国际化语言的参数名
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
/*
设置国际化请求参数为language
设置完成之后,URL中的 ?language=zh 表示读取国际化文件messages_zh.properties
*/
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
/**
* Spring在启动的时候,加载上下文的时候,会查询查询是否存在容器名称为messageSource的bean
* 如果没有就会创建一个名为messageSource的bean,然后放在上下文中
* 我们手动创建一个名为messageSource的bean,替代Spring为我们自动创建
*/
@Bean
public MessageSource messageSource() {
return new CustomMessageSource();
}
// 将我们自定义的国际化语言参数拦截器放入Spring MVC的默认配置中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
三. 校验
3.1 校验Controller
import com.example.jmw.common.config.CustomMessageSource;
import com.example.jmw.form.Test9Form;
import org.springframework.stereotype.Controller;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import java.util.Set;
@Controller
@RequestMapping("/test9")
public class Test9Controller {
// 注入校验validator
@Resource
private LocalValidatorFactoryBean validator;
// 注入自定义MessageSource
@Resource
private CustomMessageSource customMessageSource;
@GetMapping("/init")
public ModelAndView init() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test9");
return modelAndView;
}
@PostMapping("/validate")
@ResponseBody
public void validate(@RequestBody Test9Form form) {
Set<ConstraintViolation<Test9Form>> validateSet = validator.validate(form);
for (ConstraintViolation<Test9Form> violation : validateSet) {
System.out.println(violation.getMessage());
}
}
// 重新加载国际化消息
@PostMapping("/reloadMessage")
@ResponseBody
public void reloadMessage() {
customMessageSource.reload();
}
}
3.2 效果
✅✅✅进入国际化页面后,点击校验按钮
✅✅✅切换语言为日语,然后点击校验按钮
✅✅✅修改数据库数据后,点击重新加载Message按钮后,刷新页面
可以看到,我们修改的数据,已经被反映到页面上
✅✅✅将 M004 ja 这条数据删除(修改为M005,就相当于删除)之后,点击重新加载Message按钮后,刷新页面.可以看到只有code显示在页面上.
3.3 StaticMessageSource说明
- 在前面的文章中,我们自定义了一个CustomMessageSource 类继承了AbstractMessageSource 类实现了InitializingBean接口,从而实现了从数据库获取到的国际化消息和本地properties文件中的国际化消息整合的功能。
StaticMessageSource
类也能实现同样的功能,但是不推荐在生产环境中使用,并且不支持国际化消息删除- StaticMessageSource适合国际化消息测试,支持硬编码的方法添加国际化消息
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.support.StaticMessageSource;
import java.util.Locale;
@Component("messageSource")
public class CustomMessageSource extends StaticMessageSource implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// addMessage为 StaticMessageSource 父类中的方法
this.addMessage("key", Locale.CHINA, "从数据库查询来的信息");
}
}
StaticMessageSource的源码截图如下:
源码的注释也提到了
Intended for testing rather than for use in production systems.
翻译为中文就是用于测试而不是用于生产系统
.
并且所有的国际化消息最终都会缓存到messageMap中.
由于StaticMessageSource并没有提供清除map数据的方法,
因此只有当程序重启,数据库删除的国际化消息才能被反映到messageMap中.