文章目录
- 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 methodspublic 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 {@Beanpublic 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")@Configurationstatic class I18nFilterConfig {@Autowiredprivate I18nLocaleHolder i18nLocaleHolder;@Beanpublic 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}*/@Beanpublic MessageSourceProperties messageSourceProperties() {return new MessageSourceProperties();}@Beanpublic ResourceBundleMessageSource initResourceBundleMessageSource(MessageSourceProperties messageSourceProperties) {ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();resourceBundleMessageSource.setBasename(messageSourceProperties.getBasename());resourceBundleMessageSource.setDefaultEncoding(messageSourceProperties.getEncoding().name());return resourceBundleMessageSource;}@Bean@Autowiredpublic I18nMessageResource initMessageResource(ResourceBundleMessageSource resourceBundleMessageSource,I18nLocaleHolder i18NLocaleSettings) {I18nMessageResource i18nMessageResource = new I18nMessageResource(i18NLocaleSettings.getDefaultLocale());i18nMessageResource.setParentMessageSource(resourceBundleMessageSource);return i18nMessageResource;}@Bean@Autowiredpublic 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@EnableAsyncstatic class I18nInvalidMarkerConfig {@Bean("i18nExecutor")@Autowiredpublic 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;}@Beanpublic 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以允许请求继续进行。*/@Overridepublic 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中的语言设置。*/@Overridepublic 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;@Autowiredprivate List<I18nConfigDbLoader> i18NConfigDbLoaders;@Autowiredprivate 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;}@PostConstructpublic 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);}}}@Overridepublic void setResourceLoader(@NonNull ResourceLoader resourceLoader) {}@Overrideprotected MessageFormat resolveCode(@NonNull String code, @NonNull Locale locale) {String msg = getText(code, locale);return createMessageFormat(msg, locale);}@Overrideprotected 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国际化配置组件支持本地配置和数据库配置 内容分享就到这儿,写的有点粗糙。如果你有疑问或见解可以在评论区留言。