Java 后端国际化设计方案
- 前言
- 设计需求
- 设计思路
- 数据库设计
- 功能设计
- 用到的工具类
- 自定义注解
- 切面开发
- TranslationAspect
- 从请求头获取当前语言环境
- 获取当前返回值的类型
- 将返回值转为 Json String 后,统一获取其中的占位符
- 替换返回值中所有的占位符为对应语言
- 最后要保证返回值的类型正确
- 数据缓存
- 构建线程池
- 数据缓存到 Redis
- 项目启动初始化国际化数据
- 效果展示
前言
代码就不放全了,还在公司上跑着呢,就放一点非核心代码,工具类封装之类的
设计需求
- 国际化配置集中到数据库中进行管理,包含前端部分国际化
- 最好可动态添加国际化的语种
- 好用易用
- 高效
设计思路
- 利用自定义注解来启用国际化,拦截所有返回请求进行处理
- 大数据量处理使用多线程并行处理
- 国际化数据保存在 Redis 中视为热点数据
- 使用手动刷新方式,保证无缝刷新缓存
- 国际化部分数据以 Json 形式来保存,保证扩展性
- 语种以配置的形式保存,必要可添加语种
- 需要多语言切换的数据全部以占位符代替,通过自定义注解统一替换
- 当前语言环境通过前端带在请求头里给后端,后端默认为英文
数据库设计
- type: 类型,非空
- module: 模块,可为空
- label: 标签,非空
- langs: 国际化 Json String,非空
- to_web: 是否返回前端,不返回的就只是后端使用,将数据切成两半
其中 type.module.label 的组合为唯一标识
需要国际化翻译的数据保存形式
由于后端部分为自动生成的,因此 label 使用 UUID()
后端返回数据部分,比如:异常提示这部分的翻译
功能设计
用到的工具类
JsonUtils.java
自定义注解
个人认为只需要一个启动开关即可,没必要做成那种一个个接口去加
/**
* 开启国际化注解
*/
@Import(TranslationAspect.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableTranslation {
}
使用
切面开发
TranslationAspect
/**
* 国际化实现
* @Author: linjinp
* @Date: 2021/1/18 15:47
*/
@Slf4j
@Aspect
public class TranslationAspect {
// 默认语种
private final static String DEFAULT_LANGUAGE = "en";
// 切点
// 指定拦截的包路径
@Pointcut("execution(* com.xxx..*.*(..))")
private void pointcut() {}
@Around("pointcut()")
private Object around(ProceedingJoinPoint pjp) throws Throwable {
// ...
// TODO
// ...
return ...
}
从请求头获取当前语言环境
获取当前返回值的类型
/**
* 获取列表中数据类型
* @param obj
* @return
*/
private static Class getArrayListClass(Object obj) {
// 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型)
if (obj != null && ArrayList.class.equals(obj.getClass()) && ((List) obj).size() > 0) {
return ((List) obj).get(0).getClass();
}
return null;
}
/**
* 获取数据类型
* @param obj
* @return
*/
private static Class getClass(Object obj) {
// 判断空,判断类型是否为列表,判断是否有数据(无数据无法获取类型)
if (obj != null && !ArrayList.class.equals(obj.getClass())) {
return obj.getClass();
}
return null;
}
将返回值转为 Json String 后,统一获取其中的占位符
使用 StringBuilder 保存,防止大量的对象被创建
/**
* 获取字符串中所有的变量参数
* @param str 字符串/对象Json
* @return
*/
private static List<String> findParams(String str) {
List<String> params = new ArrayList<>();
// 转化为二进制
char[] chars = str.toCharArray();
// 找到标志的索引
int findIndex = -1;
for (int i = 0; i < chars.length; i ++) {
// 判断 ${ 组合
// i <= chars.length - 3 防越界,${A,假如以此结尾,$ 在 length - 3 位置
if (i <= chars.length - 3 && chars[i] == '$' && chars[i + 1] == '{') {
// 获取首个变量的下标索引
findIndex = i + 2;
}
// 判断 } 且,已经存在索引下标,防止前面单独出现 } 的情况
if (chars[i] == '}' && findIndex != -1) {
// 添加变量
params.add(new String(Arrays.copyOfRange(chars, findIndex, i)));
// 重置标识
findIndex = -1;
}
}
return params;
}
替换返回值中所有的占位符为对应语言
/**
* 数据处理
* @param lang 语言环境
* @param data 返回数据
* @param languages 语言包
* @param params 需要替换的参数列表
* @return
*/
private static StringBuilder dataProcess(String lang, StringBuilder data, List<MultiLanguage> languages, List<String> params) {
// 循环数据
for (MultiLanguage language : languages) {
// 有配置语言,非空对象,为后端使用的标签
if (StringUtils.isNotBlank(language.getLangs()) && !"{}".equals(language.getLangs())) {
for (String param : params) {
// 如果标签组合匹配
if (language.equalsCombination(param)) {
// 假如当前环境非默认语种,判断当前语种是否已经配置,如果没配置或为空,使用默认语种数据
if (!DEFAULT_LANGUAGE.equals(lang) && JsonUtils.toMap(language.getLangs()).containsKey(lang) && StringUtils.isNotBlank((String) JsonUtils.toMap(language.getLangs()).get(lang))) {
data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(lang))));
} else {
data.replace(0, data.length(), replaceRegex(data.toString(), param, StrUtil.nullToEmpty((String) JsonUtils.toMap(language.getLangs()).get(DEFAULT_LANGUAGE))));
}
}
}
}
}
return data;
}
/**
* 正则内容替换
* @param source 数据
* @param key 国际化标签
* @param value 国际化对应值
* @return
*/
private static String replaceRegex(String source, String key, String value) {
String regex = "\\$\\{"+key+"\\}";
return source.replaceAll(regex, value);
}
最后要保证返回值的类型正确
也是为了保证旧代码的兼容,比如你后来才加的国际化,这也是之前获取数据类型的原因
数据缓存
构建线程池
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
/**
* 线程池配置
*
* @Author: linjinp
* @Date: 2020/9/29 10:54
*/
@Slf4j
@Component
public class ExecutorConfig {
public static ExecutorService executor;
// 初始线程数量
private final static int DEFAULT_NUM = 5;
// 最大线程数
private final static int MAX_NUM = 10;
// 最大等待线程数
private final static int MAX_WAITING = 100;
@Bean
public ExecutorService createExecutor() {
this.executor = ExecutorBuilder.create()
// 默认初始化 5 个线程
.setCorePoolSize(DEFAULT_NUM)
// 最大线程数 10
.setMaxPoolSize(MAX_NUM)
// 最大等待线程数 100
.setWorkQueue(new LinkedBlockingQueue<>(MAX_WAITING))
.build();
log.info("\n初始化线程池\n默认初始线程数:{}\n最大线程数:{}\n最大等待线程数:{}", DEFAULT_NUM, MAX_NUM, MAX_WAITING);
return this.executor;
}
}
数据缓存到 Redis
刷新时将数据保存到 Redis 中
这里使用 多线程 + 闭锁 的方式同步进行两方的处理,闭锁保证两个线程都完成后才继续执行,返回前端成功
/**
* 刷新国际化配置缓存
* @return
*/
@ApiOperation("刷新国际化配置缓存")
@GetMapping(value = "/refresh")
public ErrorMsg<Map<String, Object>> refresh() throws InterruptedException {
// 利用闭锁保证两个线程都执行完毕后返回
final CountDownLatch countDownLatch = new CountDownLatch(2);
// 前端国际化数据,多线程处理
ExecutorConfig.executor.execute(new Runnable(() -> {
try {
// 构建前端所需的数据格式
Map<String, Object> languageMap = buildLangToWeb();
// 保存前端国际化部分数据 Map
redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_ZONE, languageMap);
} finally {
countDownLatch.countDown();
}
}));
// 后端国际化数据,多线程处理
ExecutorConfig.executor.execute(new Runnable(() -> {
try {
// 获取后端所需的数据列表
List<MultiLanguage> languageList = buildLangToJava();
// 保存后端国际化部分数据 List
redisTemplate.opsForValue().set(RedisKeyConfig.LANGUAGE_JAVA, languageList);
} finally {
countDownLatch.countDown();
}
}));
// 闭锁阻塞
countDownLatch.await();
return ErrorMsg.SUCCESS;
}
项目启动初始化国际化数据
// 开启语言翻译
@EnableTranslation
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
@Autowired
private MultiLanguageController multiLanguageController;
@Bean
public CommandLineRunner runner() {
return args -> {
log.info("开始初始化国际化数据:{}", new Date());
multiLanguageController.refresh();
log.info("国际化初始化完成:{}", new Date());
};
}
}
效果展示
中文返回
英文返回