SpringBoot实现国际化

文章目录


前言

SpringBoot 提供了国际化功能,其原理是将配置的各个语言资源文件信息,以Map 的形式进行缓存。当前端请求给定某个语言标识时(一般是放到请求头中),拿去指定的语言标识去获取响应的响应信息。在Springboot 项目启动时,由MessageSourceAutoConfiguration 类进行消息资源自动配置。

该类存在 @Conditional 条件注解,也就是说必须满足某个条件是才会进行自动装载配置。ResourceBundleCondition 类用于判断是否满足自动注入条件。 getMatchOutcome 用于返回一个 ConditionOutcome 对象,用于后续判断是否满足自动注入条件。该方法会自动读取spring.messages.basename 配置的资源文件地址信息,通过getResources 方法获取默认的文件资源。如果该资源不存在,则不满足自动注入条件。getResources 明确标注了只能从classpath*下拿去资源文件,文件类型为properties

java 复制代码
/**
         * 判断是否满足自动注入条件
         * @param context the condition context
         * @param metadata the annotation metadata
         * @return
         */
        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = cache.get(basename);
            if (outcome == null) {
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }

        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
            ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
            for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(),name)) {
                    if (resource.exists()) {
                        return ConditionOutcome.match(message.found("bundle").items(resource));
                    }
                }
            }
            return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
        }

        //读取消息资源文件,从classpath下寻找格式为properties类型的资源文件
        private Resource[] getResources(ClassLoader classLoader, String name) {
            String target = name.replace('.', '/');
            try {
                return new PathMatchingResourcePatternResolver(classLoader)
                        .getResources("classpath*:" + target + ".properties");
            }
            catch (Exception ex) {
                return NO_RESOURCES;
            }
        }

MessageSourceProperties 类则用于配置消息源的配置属性。在ResourceBundleCondition 返回条件成立的情况下,会通过注解Bean 进行注入。读取前缀带有spring.messages的配置信息。

java 复制代码
  @Bean
    @ConfigurationProperties(prefix = "spring.messages")
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

MessageSourceProperties类可配置的属性并不多,具体属性含义用途如下:

java 复制代码
#配置国际化资源文件路径,基础包名名称地址
spring.messages.basename=il8n/messages
#编码格式,默认使用UTF-8
spring.messages.encoding=UTF-8
#是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
spring.messages.always-use-message-format=false
# 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
spring.messages.use-code-as-default-message=true

MessageSourceProperties 完成读取配置之后,将会自动注入MessageSource ,而默认注入的MessageSource 的实现类ResourceBundleMessageSourceResourceBundleMessageSourceSpringBoot 实现国际化的核心。其采用的是及时加载文件的形式。即:只有当某种特定的语言要求被返回时,才会去读取资源文件,将消息内容缓存起来并通过响应码进行返回具体消息。ResourceBundleMessageSource的源码并不复杂,这里就不展开讲解。

java 复制代码
 @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        //消息资源绑定类,用于缓存资源消息
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
            //设置资源消息默认包名
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }
        if (properties.getEncoding() != null) {
            //设置编码格式
            messageSource.setDefaultEncoding(properties.getEncoding().name());
        }
        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
        Duration cacheDuration = properties.getCacheDuration();
        if (cacheDuration != null) {
            //设置消息资源过期时间
            messageSource.setCacheMillis(cacheDuration.toMillis());
        }
        //是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        //是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }

一、配置消息资源文件

消息资源文件用于存储不同国家语言响应的消息,我们通过源码知道该文件必须是properties 类型(在你不去更改源码的时候),因此我们需要在resources 资源文件下存放消息文件。这里我存放三个文件,message 基础资源文件(内容可以为空),messages_en_US.properties 英文文件,messages_zh_CN.properties 中文文件。

messages_zh_CN.propertiesmessages_en_US.properties 内容如下:


二、配置消息源

这里采用的是以application.properties 形式进行配置,您也可以采用yaml文件形式进行配置:

java 复制代码
#配置国际化资源文件路径
spring.messages.basename=il8n/messages
#编码格式,默认使用UTF-8
spring.messages.encoding=UTF-8
#是否总是应用MessageFormat规则解析信息,即使是解析不带参数的消息
spring.messages.always-use-message-format=false
# 是否使用消息代码作为默认消息,而不是抛出NoSuchMessageException.建议在开发测试时开启,避免不必要的抛错而影响正常业务流程
spring.messages.use-code-as-default-message=true

三、配置消息拦截器

拦截器用于从请求头中获取语言标识,以便于后续根据语言标识响应不同的响应信息。

java 复制代码
package com.il8n.config;

import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Author: Greyfus
 * @Create: 2024-01-12 13:06
 * @Version: 1.0.0
 * @Description:国际化拦截器
 */
@Setter
@Getter
public class IL8nLangInterceptor extends LocaleChangeInterceptor {

    private String langHeader;

    @Override
    public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws ServletException {
        String locale = request.getHeader(getLangHeader());
        if (locale != null) {
            if (iL8nCheckHttpMethod(request.getMethod())) {
                LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
                if (localeResolver == null) {
                    throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
                }
                try {
                    localeResolver.setLocale(request, response, parseLocaleValue(locale));
                } catch (IllegalArgumentException ex) {
                    if (isIgnoreInvalidLocale()) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Ignoring invalid locale value [" + locale + "]: " + ex.getMessage());
                        }
                    } else {
                        throw ex;
                    }
                }
            }
        }
        // Proceed in any case.
        return true;
    }

    public boolean iL8nCheckHttpMethod(String currentMethod) {
        String[] configuredMethods = getHttpMethods();
        if (ObjectUtils.isEmpty(configuredMethods)) {
            return true;
        }
        for (String configuredMethod : configuredMethods) {
            if (configuredMethod.equalsIgnoreCase(currentMethod)) {
                return true;
            }
        }
        return false;
    }

}

注入消息拦截器,并设置默认语言为中文

java 复制代码
package com.il8n.config;

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;

/**
 * @Author: Greyfus
 * @Create: 2024-01-12 00:22
 * @Version: 1.0.0
 * @Description:语言国际化配置
 */
@Configuration
public class IL8nLangConfig implements WebMvcConfigurer {

    private static final String LANG_HEADER = "lang";
    
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        sessionLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return sessionLocaleResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        IL8nLangInterceptor lci = new IL8nLangInterceptor();
        //设置请求的语言变量
        lci.setLangHeader(LANG_HEADER);
        return lci;
    }

    /**
     * 注册拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

}

四、编写消息工具类

消息工具类用于通过指定的消息码获取对应的响应消息

java 复制代码
package com.il8n.config;

import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @Author: Greyfus
 * @Create: 2024-01-12 00:36
 * @Version: 1.0.0
 * @Description: 工具
 */
@Component
public class SpringUtils implements ApplicationContextAware {
    @Getter
    private static ApplicationContext applicationContext;

    public SpringUtils() {
    }

    public void setApplicationContext(@NotNull ApplicationContext applicationContext) {
        SpringUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }

    public static <T> Map<String, T> getBeansOfType(Class<T> type) {
        return applicationContext.getBeansOfType(type);
    }

    public static String[] getBeanNamesForType(Class<?> type) {
        return applicationContext.getBeanNamesForType(type);
    }

    public static String getProperty(String key) {
        return applicationContext.getEnvironment().getProperty(key);
    }

    public static String[] getActiveProfiles() {
        return applicationContext.getEnvironment().getActiveProfiles();
    }

}
java 复制代码
package com.il8n.config;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;

/**
 * @Author: Greyfus
 * @Create: 2024-01-12 00:29
 * @Version: 1.0.0
 * @Description: IL8N语言转换工具
 */
public class IL8nMessageUtils {


    private static final MessageSource messageSource;

    static {
        messageSource = SpringUtils.getBean(MessageSource.class);
    }

    /**
     * 获取国际化语言值
     *
     * @param messageCode
     * @param args
     * @return
     */
    public static String message(String messageCode, Object... args) {
        return messageSource.getMessage(messageCode, args, LocaleContextHolder.getLocale());
    }
}

五、测试国际化

编码一个Controller用于模拟测试效果,代码如下:

java 复制代码
package com.il8n.controller;

import com.il8n.config.IL8nMessageUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: DI.YIN
 * @Date: 2025/1/6 9:37
 * @Version:
 * @Description:
 **/
@RestController
@RequestMapping("/mock")
public class IL8nTestController {

    @RequestMapping(value = "/login")
    public ResponseEntity<String> login(@RequestParam(value = "userName") String userName, @RequestParam(value = "password") String password) {
        if (!"admin".equals(userName) || !"admin".equals(password)) {
            return new ResponseEntity<>(IL8nMessageUtils.message("LoginFailure", (Object) null), HttpStatus.OK);
        }
        return new ResponseEntity<>(IL8nMessageUtils.message("loginSuccess", userName), HttpStatus.OK);
    }
}

消息拦截器会尝试从请求头中获取属性为lang 的值,将其作为语言标识,因此我们在使用PostMan 模拟时,需要在请求头中增加lang 属性。

模拟结果如下:

中文语言标识:

英文语言标识:

相关推荐
一只叫煤球的猫5 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9656 小时前
tcp/ip 中的多路复用
后端
bobz9656 小时前
tls ingress 简单记录
后端
皮皮林5517 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友7 小时前
什么是OpenSSL
后端·安全·程序员
bobz9657 小时前
mcp 直接操作浏览器
后端
前端小张同学10 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook10 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康11 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在11 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net