SpringBoot国际化配置组件支持本地配置和数据库配置


在这里插入图片描述

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应用程序的国际化,即根据用户的语言设置显示对应的国际化资源文件。

  1. 从请求的cookie或header中获取语言设置。
  2. 将语言设置存储到i18nLocaleHolder中,以便在后续的请求处理中使用。
  3. 在请求处理完成后,清除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国际化配置组件支持本地配置和数据库配置 内容分享就到这儿,写的有点粗糙。如果你有疑问或见解可以在评论区留言。

<think>首先,用户的问题是关于在Spring Boot 3中实现国际化支持配置文件、缓数据库加载语言资源。用户提到了“国际化”、“多语言支持”,并特别指出要从配置文件、缓数据库加载语言。 我从系统级指令中知道,我需要: - 使用$...$格式表示行内数学表达式(但在这个上下文中,可能不涉及数学表达式)。 - 独立公式用$$...$$格式(同样,可能不适用)。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在引用的段落末尾添加引用标识。 - 回答结构清晰,逐步解决问题。 - 参考站内引用:[^1]、[^2]、[^3]、[^4]。 站内引用内容: - [^1]: 关于多语言支持Spring Boot实现动态多语言支持。 - [^2]: Spring Boot国际化机制常见问题。 - [^3]: Spring Boot国际化配置组件支持本地数据库配置。 - [^4]: Redis缓支持。 这些引用是相关的,我应该引用它们来支持我的回答。 用户的问题核心:在Spring Boot 3中实现国际化支持配置文件、缓数据库加载语言。 关键点: - **国际化i18n)**:使应用程序支持多种语言。 - **加载来源**:配置文件(如.properties文件)、缓(如Redis)、数据库。 - **Spring Boot 3**:确保使用最新版本。 标准Spring Boot国际化通常使用消息源(MessageSource),如ResourceBundleMessageSource,它从.properties文件加载消息。但用户想扩展这个,支持从缓数据库加载。 步骤来回答: 1. **介绍Spring Boot国际化基础**:解释标准方法。 2. **扩展加载来源**: - 配置文件:标准方式。 - 缓:使用Redis等缓检索消息。 - 数据库:从数据库表加载消息。 3. **实现细节**:提供代码示例或配置步骤。 4. **整合**:如何使MessageSource支持这些来源。 5. **引用相关资源**:引用[^1]、[^2]、[^3]、[^4]来支持。 结构回答: - **引言**:简述问题。 - **步骤详解**:逐步实现。 - **代码示例**:提供Spring Boot配置代码。 - **总结**:强调关键点。 - **相关问题**:生成相关问题。 生成相关问题: - 基于用户问题,生成3-5个相关问题。 - 例如:如何配置?如何处理数据库加载?性能考虑? 确保中文回答。 现在,构建回答框架: 1. **开头**:确认问题并概述解决方案。 2. **Spring Boot国际化基础**:解释标准ResourceBundleMessageSource。 3. **支持配置文件**:标准方法。 4. **支持数据库加载**:使用自定义MessageSource,如从数据库加载。 5. **支持加载**:集成缓,如Redis。 6. **整合所有来源**:如何优先或组合来源。 7. **代码示例**:提供关键代码片段。 8. **引用**:自然地添加引用标识。 9. **结尾**:总结。 10. **相关问题**:列出相关问题。 引用标识: - 在引用段落末尾添加[^1], [^2], 等。 - 例如,当讨论多语言支持时,引用[^1];当讨论国际化机制时,引用[^2];当讨论数据库配置时,引用[^3];当讨论缓时,引用[^4]。 代码示例:使用Markdown代码块。</think>### Spring Boot 3 国际化实现:支持配置文件、缓数据库加载语言资源 在 Spring Boot 3 中实现国际化i18n),支持配置文件、缓(如 Redis)数据库加载语言资源,是一种常见的需求,尤其适用于需要动态更新语言消息的全球化应用场景。Spring Boot 提供了强大的国际化机制,通过 `MessageSource` 接口实现消息加载。但标准实现仅支持文件系统(如 `.properties` 文件),因此我们需要扩展它来支持数据库。以下我将逐步指导您实现这一功能,确保结构清晰、可靠。整个过程基于 Spring Boot 3.x Java 17+。 #### 步骤 1: 理解 Spring Boot 国际化基础 Spring Boot国际化机制通过 `ResourceBundleMessageSource` 或 `ReloadableResourceBundleMessageSource` 实现,默认从类路径下的 `messages.properties` 文件加载消息(例如 `messages_en_US.properties`)。这适用于静态配置场景[^2]。 - **关键接口**:`MessageSource` 定义了方法如 `getMessage(String code, Object[] args, Locale locale)`,用于获取本地化消息。 - **配置文件加载**:标准方式是将消息储在 `src/main/resources` 目录下的 `.properties` 文件中。例如: ```properties # messages_en.properties greeting=Hello, {0}! # messages_zh_CN.properties greeting=你好, {0}! ``` 在应用中,通过 `@Autowired` 注入 `MessageSource` 并使用它。 然而,用户需求要求扩展来源,支持动态加载[^1]。 #### 步骤 2: 扩展支持数据库加载语言资源 为了从数据库加载消息,我们需要实现一个自定义的 `MessageSource`,覆盖 `getMessage` 方法,优先查询数据库。这允许动态更新消息而无需重启应用。 - **数据库设计**:创建一个表储消息键值对。例如: ```sql CREATE TABLE i18n_messages ( id INT AUTO_INCREMENT PRIMARY KEY, message_key VARCHAR(255) NOT NULL, message_value TEXT NOT NULL, locale VARCHAR(10) NOT NULL -- 如 'en_US' 或 'zh_CN' ); ``` - **实现自定义 MessageSource**:创建一个类继承 `AbstractMessageSource`,并重写关键方法。使用 Spring Data JPA 简化数据库操作。 ```java import org.springframework.context.support.AbstractMessageSource; import org.springframework.stereotype.Component; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import java.text.MessageFormat; import java.util.Locale; @Component("databaseMessageSource") public class DatabaseMessageSource extends AbstractMessageSource { @PersistenceContext private EntityManager entityManager; @Override protected MessageFormat resolveCode(String code, Locale locale) { // 查询数据库获取消息 String message = entityManager.createQuery( "SELECT m.messageValue FROM I18nMessage m WHERE m.messageKey = :key AND m.locale = :locale", String.class) .setParameter("key", code) .setParameter("locale", locale.toString()) .getSingleResult(); return new MessageFormat(message, locale); } } ``` - **配置 Spring Boot**:在 `application.properties` 中启用 JPA 数据库连接: ```properties spring.datasource.url=jdbc:mysql://localhost:3306/your_db spring.datasource.username=root spring.datasource.password=password spring.jpa.hibernate.ddl-auto=update ``` 然后在主配置类中注册自定义 `MessageSource`: ```java import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class I18nConfig { @Bean public MessageSource messageSource() { return new DatabaseMessageSource(); // 优先使用数据库源 } } ``` 这样,当调用 `getMessage` 时,系统会优先查询数据库。如果消息不在,可以回退到文件源[^3]。 #### 步骤 3: 集成缓加载语言资源 为了提高性能,我们可以添加缓层(如 Redis),避免频繁访问数据库Spring Boot 3 通过 `spring-boot-starter-data-redis` 支持 Redis 缓。 - **添加依赖**:在 `pom.xml` 中引入 Redis starter: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` - **配置**:在 `application.properties` 中设置 Redis: ```properties spring.redis.host=localhost spring.redis.port=6379 ``` - **修改自定义 MessageSource 以支持**:更新 `DatabaseMessageSource`,使用 `@Cacheable` 注解缓查询结果。 ```java import org.springframework.cache.annotation.Cacheable; // ... 其他导入 public class DatabaseMessageSource extends AbstractMessageSource { @PersistenceContext private EntityManager entityManager; @Cacheable(value = "i18nMessages", key = "{#code, #locale.toString()}") @Override protected MessageFormat resolveCode(String code, Locale locale) { String message = entityManager.createQuery(/* SQL 查询同上 */) .getSingleResult(); return new MessageFormat(message, locale); } } ``` 这里,`@Cacheable` 将消息缓到 Redis,键为 `code + locale`。下次请求时,直接从缓获取,减少数据库压力。缓过期策略可通过 `@CacheConfig` 配置[^4]。 #### 步骤 4: 整合配置文件、缓数据库加载 为了支持多来源(配置文件、缓数据库),我们需要实现一个 `CompositeMessageSource`,定义加载优先级(例如:先查缓,再数据库,最后文件)。 - **创建组合消息源**:使用 Spring 的 `DelegatingMessageSource` 管理多个来源。 ```java import org.springframework.context.support.DelegatingMessageSource; import org.springframework.context.support.ResourceBundleMessageSource; // ... 其他导入 @Configuration public class I18nConfig { @Bean public MessageSource messageSource() { DelegatingMessageSource delegatingSource = new DelegatingMessageSource(); // 设置优先级:缓/数据库源 > 文件源 delegatingSource.addMessageSource(cacheDatabaseSource()); // 步骤 2-3 中的自定义源 delegatingSource.addMessageSource(fileMessageSource()); // 文件源 delegatingSource.setParentMessageSource(cacheDatabaseSource()); // 默认父源 return delegatingSource; } @Bean public MessageSource fileMessageSource() { ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBasename("messages"); source.setDefaultEncoding("UTF-8"); return source; } @Bean public MessageSource cacheDatabaseSource() { return new DatabaseMessageSource(); // 包含缓逻辑 } } ``` - **加载顺序**:当调用 `getMessage` 时: 1. 先检查缓(如果启用)。 2. 缓未命中则查询数据库。 3. 数据库未命中则回退到 `.properties` 文件。 4. 如果所有来源都失败,使用默认消息或抛出异常。 - **动态更新**:支持热更新:当数据库消息变化时,通过 Redis Pub/Sub 或 Spring 事件刷新缓。例如: ```java // 在更新消息的服务类中 @CacheEvict(value = "i18nMessages", key = "{#key, #locale}") public void updateMessage(String key, String value, Locale locale) { // 更新数据库 // 然后发布事件刷新缓 } ``` #### 最佳实践注意事项 - **性能优化**:缓层显著减少数据库访问,适合高并发场景。Redis 的响应式支持(引用 `spring-boot-starter-data-redis-reactive`)可用于异步应用[^4]。 - **错误处理**:添加回退机制,例如在自定义源中捕获异常并回退到文件源。 - **测试**:使用 Spring Boot Test 验证多语言加载: ```java @SpringBootTest public class I18nTest { @Autowired private MessageSource messageSource; @Test public void testMessageLoading() { String message = messageSource.getMessage("greeting", new Object[]{"User"}, Locale.CHINA); Assertions.assertEquals("你好, User!", message); } } ``` - **引用支持**:本文方法参考了 Spring Boot 官方文档社区实践。动态多语言支持是现代应用的关键需求,尤其在全球化市场中[^1]。数据库配置组件可简化实现[^3]。 #### 总结 在 Spring Boot 3 中实现国际化支持配置文件、缓数据库加载,核心在于扩展 `MessageSource` 接口: - **配置文件加载**:使用标准 `ResourceBundleMessageSource`。 - **数据库加载**:实现自定义 `MessageSource` 查询数据库表。 - **缓加载**:集成 Redis 缓消息结果。 - **整合来源**:通过 `DelegatingMessageSource` 管理优先级。 此方案确保消息动态更新、高性能,适用于电商、SaaS 等场景。如果您遇到具体问题(如缓配置),可参考 GitHub 上的开源组件[^3]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰点.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值