文章目录
- 0. 前言
- i18n-spring-boot-starter
- 1. 使用方式
- 0.引入依赖
- 1.配置项
- 2.初始化国际化配置表
- 3.如何使用
- 2. 核心源码
- 实现一个拦截器I18nInterceptor
- I18nMessageResource 加载国际化配置
- 3.源码地址
0. 前言
写个了原生的SpringBoot国际化配置组件支持本地配置和数据库配置
背景:最近花时间把项目用到的国际化组件Starter 重构了一下,使用更简单。基本上支持从本地配置读取和数据库配置读取,支持web端和小程序等移动端的国际化需求。
i18n-spring-boot-starter
1. 使用方式
Spring Boot 国际化组件
0.引入依赖
代码在本地打包后
给需要国际化的工程引入
<dependency>
<groupId>com.bdkjzx.project</groupId>
<artifactId>i18n-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
1.配置项
#添加国际化
spring.ex.i18n.enable=true
# 如果未翻译是否将code 初始化入库
spring.ex.i18n.mark=false
spring.ex.i18n.default-locale=zh-CN
spring.ex.i18n.data-source=primary
spring.ex.i18n.config-table=config_i18n_message
2.初始化国际化配置表
CREATE TABLE `config_i18n_message` (
`code` varchar(128) NOT NULL,
`zh-CN` varchar(128) DEFAULT NULL,
`zh-TW` varchar(128) DEFAULT NULL,
`en-US` varchar(1024) DEFAULT NULL COMMENT '英文',
PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='国际化配置表'
如果本地配置的话使用原生配置方式.缺点是需要手动更新,并且每个服务都需要配置。建议使用数据库表配置
messages_zh_CN.properties , messages_en_US.properties
3.如何使用
I.n("操作成功")
或者在返回的统一结果对象上,以下是个示例,你需要加在你的项目的统一响应中
public class ApiResponse<T> {
private int code;
private String message;
private T data;
private ErrorDetails error;
public ApiResponse() {
}
/**
* message给消息进行国际化包装
* @param message
*/
public ApiResponse(int code, String message, T data, ErrorDetails error) {
this.code = code;
this.message = I.n(message);
this.data = data;
this.error = error;
}
// Getter and Setter methods
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
/**
* 给消息进行国际化包装
* @param message
*/
public void setMessage(String message) {
this.message = I.n(message);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public ErrorDetails getError() {
return error;
}
public void setError(ErrorDetails error) {
this.error = error;
}
}
5.扩展请看入口
com.bdkjzx.project.i18n.config.I18nAutoConfig
2. 核心源码
package com.bdkjzx.project.i18n.config;
import com.bdkjzx.project.i18n.I18nHolder;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.ex.i18n")
@Setter
@Getter
public class I18nProperties {
/**
* 是否启用国际化功能:<br>
* - 启用:会创建和国际化相关的数据源、缓存等等;<br>
* - 不启用:{@link I18nHolder} 可以正常使用,返回原值,不会创建国际化相关的各种Bean<br>
*
* 默认:不启用,需要手动开启
*/
private Boolean enable = false;
/**
* 国际化数据表所在的数据源,入需指定,则写入数据源名称。<br>
* 此配置的作用是允许多个服务通过共享一个i18n配置表,从而共用一套i18n翻译。<br>
* 默认为空,表示使用primary数据源。
*/
private String dataSource = "primary";
/**
* 默认地区(语言)
*/
private String defaultLocale = "zh_CN";
/**
* 查询i18n配置表的名称,用于自定义修改表。<br>
* 默认:config_i18n_message
*/
private String configTable = "config_i18n_message";
/**
* i18n配置表的字段名。根据i18n配置表决定此配置<br>
* 默认:code
*/
private String configCodeColumn = "code";
/**
* i18n缓存更新时间(小时数),会提供手工刷新缓存的入口,所以不必频繁刷新<br>
* 默认值为-1,表示长期有效。<br>
*/
private Integer cacheHours = -1;
/**
* 当未找到i18n的code时,是否将其记录到表中,以便统一处理<br>
* 默认:关闭
*/
private Boolean mark = false;
/**
* 用于记录无效code的线程池缓冲区大小
*/
private Integer markPoolSize = 2000;
/**
* 是否在 {@link com.bdkjzx.project.i18n.repository.I18nMessageResource} 未找到配置时,再使用Spring默认方案,
* 从本地加载国际化资源。
* 默认:关闭
*/
private Boolean useLocale = false;
}
package com.bdkjzx.project.i18n.config;
import com.bdkjzx.project.i18n.I18nHolder;
import com.bdkjzx.project.i18n.filter.I18nFilter;
import com.bdkjzx.project.i18n.repository.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.context.MessageSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import com.bdkjzx.project.i18n.interceptor.I18nInterceptor;
import javax.sql.DataSource;
import java.util.Locale;
import java.util.concurrent.Executor;
@Configuration
@EnableConfigurationProperties({I18nProperties.class})
@Slf4j
public class I18nAutoConfig {
@Bean
public I18nHolder getI18nUtil(@Autowired(required = false) I18nMessageResource messageSource,
@Autowired(required = false) I18nLocaleHolder i18nLocaleHolder,
@Autowired I18nProperties i18NProperties) {
// 不论是否启用都会配置,保证这个工具类不会报错
return i18NProperties.getEnable() ? new I18nHolder(messageSource, i18nLocaleHolder) : new I18nHolder();
}
@ConditionalOnProperty(prefix = "spring.ex.i18n", name = "enable", havingValue = "true")
@Configuration
static class I18nFilterConfig {
@Autowired
private I18nLocaleHolder i18nLocaleHolder;
@Bean
public I18nFilter i18nFilter() {
I18nFilter i18nFilter = new I18nFilter();
I18nInterceptor interceptor = new I18nInterceptor();
interceptor.setI18nLocaleHolder(i18nLocaleHolder);
i18nFilter.setI18nInterceptor(interceptor);
return i18nFilter;
}
}
@ConditionalOnProperty(prefix = "spring.ex.i18n", name = "enable", havingValue = "true")
@Configuration
@EnableCaching
@ComponentScan("com.bdkjzx.project.i18n")
static class I18nResourceConfig {
/**
* 采用默认的配置文件配置 messages开头的文件,编码为utf8<br>
* 如 messages_zh_CN.properties , messages_en_US.properties
*
* @return {@link MessageSourceProperties}
*/
@Bean
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public ResourceBundleMessageSource initResourceBundleMessageSource(MessageSourceProperties messageSourceProperties) {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasename(messageSourceProperties.getBasename());
resourceBundleMessageSource.setDefaultEncoding(messageSourceProperties.getEncoding().name());
return resourceBundleMessageSource;
}
@Bean
@Autowired
public I18nMessageResource initMessageResource(ResourceBundleMessageSource resourceBundleMessageSource,
I18nLocaleHolder i18NLocaleSettings) {
I18nMessageResource i18nMessageResource = new I18nMessageResource(i18NLocaleSettings.getDefaultLocale());
i18nMessageResource.setParentMessageSource(resourceBundleMessageSource);
return i18nMessageResource;
}
@Bean
@Autowired
public I18nLocaleHolder getI18nLocaleSetting(I18nProperties i18nProperties) {
Locale locale;
try {
locale = new Locale.Builder()
.setLanguageTag(i18nProperties.getDefaultLocale().replace("_", "-").toLowerCase())
.build();
} catch (Exception e) {
log.error(String.format("解析默认语言时出现错误, setting = %s", i18nProperties.getDefaultLocale()), e);
throw new IllegalArgumentException("解析默认语言时出现错误,请查看日志");
}
return new I18nLocaleHolder(locale);
}
@Bean(name = "i18nJdbcTemplate")
@ConditionalOnMissingBean(name = "i18nJdbcTemplate")
public JdbcTemplate getJdbcTemplate(@Autowired(required = false) @Qualifier("i18nDataSource") DataSource i18nDataSource) {
try {
if (i18nDataSource == null) {
log.error("未配置国家化数据源,请使用@Bean构造一个名为i18nDataSource的DataSource或者直接重新此方法");
}
return new JdbcTemplate(i18nDataSource);
} catch (BeansException e) {
log.error("无效的数据源{}", i18nDataSource, e);
throw new IllegalArgumentException("创建数据源时出现错误,请查看日志");
}
}
@Autowired
@Bean(name = "defaultI18nDataLoadService")
public I18nConfigDbLoader getI18nDataLoadService(I18nProperties i18nProperties,
@Qualifier("i18nJdbcTemplate") JdbcTemplate jdbcTemplate) {
return new SimpleI18NConfigDbLoaderImpl(i18nProperties.getConfigCodeColumn(),
i18nProperties.getConfigTable(), jdbcTemplate);
}
@Autowired
@Bean(name = "i18nCacheManager")
public CacheManager getCacheManager(I18nProperties i18nProperties) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
if (i18nProperties.getCacheHours() > 0) {
// 缓存创建后,经过固定时间(小时),更新
caffeineCacheManager.setCacheSpecification(String.format("refreshAfterWrite=%sH", i18nProperties.getCacheHours()));
}
return caffeineCacheManager;
}
/**
* 线程池配置
*/
@ConditionalOnProperty(prefix = "spring.ex.i18n", name = "mark", havingValue = "true")
@Configuration
@EnableAsync
static class I18nInvalidMarkerConfig {
@Bean("i18nExecutor")
@Autowired
public Executor getAsyncExecutor(I18nProperties i18NProperties) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(0);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(i18NProperties.getMarkPoolSize());
executor.setThreadNamePrefix("i18n-executor-");
executor.initialize();
return executor;
}
@Bean
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
}
}
实现一个拦截器I18nInterceptor
作用是实现国际化(i18n)功能的拦截器。用于处理Web应用程序的国际化,即根据用户的语言设置显示对应的国际化资源文件。
- 从请求的cookie或header中获取语言设置。
- 将语言设置存储到i18nLocaleHolder中,以便在后续的请求处理中使用。
- 在请求处理完成后,清除i18nLocaleHolder中的语言设置。
package com.bdkjzx.project.i18n.interceptor;
import com.bdkjzx.project.i18n.repository.I18nLocaleHolder;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import static org.slf4j.LoggerFactory.getLogger;
/**
* 国际化拦截器,用于处理Web应用的国际化(i18n)。
*/
@Slf4j
public class I18nInterceptor implements HandlerInterceptor {
private I18nLocaleHolder i18nLocaleHolder;
private final Map<String, Locale> localeMap = new HashMap<>(8);
private static final String NAME_OF_LANGUAGE_SETTING = "lang";
/**
* 在实际处理程序方法调用之前执行的预处理方法。
* 从请求的cookie或header中获取语言设置,并将其设置到i18nLocaleHolder中。
* 如果语言设置为空或无效,则返回true以允许请求继续进行。
*/
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
String lang = getLangFromCookies(request);
if (StringUtils.isEmpty(lang)) {
lang = getLangFromHeader(request);
}
if (StringUtils.isEmpty(lang)) {
return true;
}
try {
i18nLocaleHolder.setThreadLocale(getLocaleByLang(lang));
} catch (Exception e) {
log.error("无效的语言设置:{}", lang, e);
}
return true;
}
/**
* 在完成请求处理后执行的方法。
* 清除i18nLocaleHolder中的语言设置。
*/
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) {
try {
i18nLocaleHolder.clear();
} catch (Exception e) {
log.error("清理语言设置时遇到错误:", e);
}
}
public I18nLocaleHolder getI18nLocaleHolder() {
return i18nLocaleHolder;
}
public void setI18nLocaleHolder(I18nLocaleHolder i18nLocaleHolder) {
this.i18nLocaleHolder = i18nLocaleHolder;
}
/**
* 根据语言设置获取Locale对象。
*
* @param lang 语言设置
* @return Locale对象
*/
private Locale getLocaleByLang(String lang) {
return Optional.ofNullable(localeMap.get(lang))
.orElseGet(() -> {
Locale locale = new Locale.Builder().setLanguageTag(lang).build();
localeMap.put(lang, locale);
return locale;
});
}
/**
* 从cookie中获取国际化语言设置。
*
* @param request HttpServletRequest对象
* @return 国际化语言设置
*/
private static String getLangFromCookies(HttpServletRequest request) {
String lang = Optional.ofNullable(request.getCookies())
.flatMap(cookies -> Arrays.stream(cookies)
.filter(cookie -> NAME_OF_LANGUAGE_SETTING.equals(cookie.getName()))
.findFirst())
.map(Cookie::getValue)
.orElse("");
return lang;
}
/**
* 从header中获取国际化语言设置。
*
* @param request HttpServletRequest对象
* @return 国际化语言设置
*/
private String getLangFromHeader(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
return Optional.ofNullable(acceptLanguage)
.map(lang -> lang.split(","))
.filter(array -> array.length > 0)
.map(array -> array[0])
.orElse("");
}
}
I18nMessageResource 加载国际化配置
支持本地和数据库
package com.bdkjzx.project.i18n.repository;
import com.bdkjzx.project.i18n.config.I18nProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.text.MessageFormat;
import java.util.*;
import java.util.function.BiFunction;
@Slf4j
public class I18nMessageResource extends AbstractMessageSource implements ResourceLoaderAware {
private final Locale defaultLocale;
@Autowired
private List<I18nConfigDbLoader> i18NConfigDbLoaders;
@Autowired
private I18nProperties i18NProperties;
@Lazy
@Autowired(required = false)
private I18nConfigDbLoader i18nConfigDbLoader;
private final List<BiFunction<String, Locale, String>> getTextFunctionList = new ArrayList<>();
public I18nMessageResource(Locale defaultLocale) {
this.defaultLocale = defaultLocale;
}
@PostConstruct
public void init() {
if (this.i18NProperties.getEnable()) {
getTextFunctionList.add(this::normalFinder);
getTextFunctionList.add(this::languageFinder);
getTextFunctionList.add(this::defaultLocaleFinder);
if (i18NProperties.getUseLocale() && getParentMessageSource() != null) {
getTextFunctionList.add(this::localFinder);
getTextFunctionList.add(this::localDefaultFinder);
}
}
}
@Override
public void setResourceLoader(@NonNull ResourceLoader resourceLoader) {
}
@Override
protected MessageFormat resolveCode(@NonNull String code, @NonNull Locale locale) {
String msg = getText(code, locale);
return createMessageFormat(msg, locale);
}
@Override
protected String resolveCodeWithoutArguments(@NonNull String code, @NonNull Locale locale) {
return getText(code, locale);
}
/**
* 这是加载国际化变量的核心方法,先从自己控制的内存中取,取不到了再到资源文件中取
*
* @param code 编码
* @param locale 本地化语言
* @return 查询对应语言的信息
*/
private String getText(String code, Locale locale) {
String result = getTextWithOutMark(code, locale);
if (StringUtils.isEmpty(result)) {
return result;
}
// 确实没有这项配置,确定是否要记录
logger.warn("未找到国际化配置:" + code);
if (i18NProperties.getMark()) {
i18nConfigDbLoader.markInvalidCode(code);
}
//如果最终还是取不到,返回了NULL,则外面会用默认值,如果没有默认值,最终会返回给页面变量名称,所以变量名称尽量有含义,以作为遗漏配置的最后保障
return code;
}
public String getTextWithOutMark(String code, Locale locale) {
String result = "";
// 从 function list中依次使用各种策略查询
for (BiFunction<String, Locale, String> func : getTextFunctionList) {
result = func.apply(code, locale);
if (!StringUtils.isEmpty(result)) {
return result;
}
}
return result;
}
/**
* 从指定locale获取值
*
* @param code i18n code
* @param locale 语言
* @return 查询对应语言的信息
*/
private String findValueFromLocale(String code, Locale locale) {
String resultValue;
for (I18nConfigDbLoader i18NConfigDbLoader : i18NConfigDbLoaders) {
// 在loadE6I18nDictByLocaleEntity中做过缓存了
resultValue = Optional.ofNullable(i18NConfigDbLoader.loadI18nDictByLocaleEntity())
.flatMap(localeMap -> Optional.ofNullable(localeMap.get(locale))
.map(codeMap -> codeMap.get(code)))
.orElse(null);
if (!org.springframework.util.StringUtils.isEmpty(resultValue)) {
return resultValue;
}
}
return null;
}
// ====================================== 查询字符的五种策略,加入function list ======================================
/**
* 第一种情况:通过期望的语言类型查找
*
* @param code 国际化代码
* @param locale 语言
* @return 没找到时返回null
*/
private String normalFinder(String code, Locale locale) {
return findValueFromLocale(code, locale);
}
/**
* 第二种情况,如果期望是 语言-国家 没有找到,那么尝试只找一下语言,比如zh-tw没找到,那就尝试找一下zh
*
* @param code 国际化代码
* @param locale 语言
* @return 没找到时返回null
*/
private String languageFinder(String code, Locale locale) {
if (locale.getLanguage() != null) {
return findValueFromLocale(code, Locale.forLanguageTag(locale.getLanguage()));
}
return null;
}
/**
* 第三种情况,如果没有找到 且不是默认语言包,则取默认语言包
*
* @param code 国际化代码
* @param locale 语言
* @return 没找到时返回null
*/
private String defaultLocaleFinder(String code, Locale locale) {
if (!Objects.equals(locale, defaultLocale)) {
return findValueFromLocale(code, defaultLocale);
}
return null;
}
/**
* 第四种情况,通过以上三种方式都没找到,那么尝试从本地配置文件加载期望的语言类型是否有
*
* @param code 国际化代码
* @param locale 语言
* @return 没找到时返回null
*/
private String localFinder(String code, Locale locale) {
String value = Objects.requireNonNull(getParentMessageSource()).getMessage(code, null, null, locale);
if (logger.isDebugEnabled() && !StringUtils.isEmpty(value)) {
logger.debug("从配置文件" + locale.toString() + "找到变量" + code + "=" + value);
}
return value;
}
/**
* 第五种情况,如果没有找到,则从本地配置文件加载默认的语言类型是否有
*
* @param code 国际化代码
* @param locale 语言
* @return 没找到时返回null
*/
private String localDefaultFinder(String code, Locale locale) {
if (!Objects.equals(locale, defaultLocale)) {
return this.localFinder(code, defaultLocale);
}
return null;
}
}
pom 文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bdkjzx.project</groupId>
<artifactId>i18n-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>i18n-spring-boot-starter</name>
<description>Spring boot 国际化配置</description>
<properties>
<java.version>8</java.version>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--版本支持到2.7.x-->
<spring-boot.version>2.0.3.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.源码地址
https://github.com/wangshuai67/i18n-spring-boot-starter/
大家好,我是冰点,今天的原生的SpringBoot国际化配置组件支持本地配置和数据库配置 内容分享就到这儿,写的有点粗糙。如果你有疑问或见解可以在评论区留言。