java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot 国际化适配

SpringBoot 国际化适配方案使用解决方案

作者:小码农叔叔

本文以微服务项目为例,具体来说,以springboot框架为例进行说明如何在微服务项目中运用国际化解决方案,感兴趣的朋友跟随小编一起看看吧

一、前言

随着一个系统的规模做上去之后,国际化的问题就会逐渐暴露出来,简单来说,当你的系统面对的不再是本国的用户,而要面临海外用户时,系统必须要能适配国际化。系统国际化这个事情,互联网经历了多年的发展之后,尤其是移动端的适配方案已经很成熟,本文以微服务项目为例,具体来说,以springboot框架为例进行说明如何在微服务项目中运用国际化解决方案。

二、国际化概述

2.1 微服务中的国际化是什么

微服务中的国际化(Internationalization,简称I18n)‌是指设计和开发产品的过程,使得它们能够适应多种语言和文化环境,而不需要进行大量的代码更改。这通常涉及到创建一个基础版本的产品,然后通过配置和资源文件来添加对不同语言和地区的支持。当产品需要在新的地理区域或语言环境中使用时,只需要添加或更新相应的资源文件,而不需要修改产品本身的代码‌。

2.1.1 国际化概念

2.1.2 为什么需要国际化

在微服务架构中,系统国际化尤为重要,原因如下:

2.2 微服务中常用的国际化方法

在微服务设计中,国际化的实现方案有多种,下面列举了常用的几种方案

2.2.1 资源文件分离

将文本、图像等资源分离出来,存储在不同的文件中,每个文件对应一种语言。

2.2.2 使用国际化框架

使用现有的国际化框架,如 Spring Framework 的 ResourceBundleMessageSource,简化国际化的开发和适配工作。

2.2.3 使用动态模板

在springboot框架中,可以使用模板引擎(如 Thymeleaf、Freemarker)动态生成多语言内容。

2.2.4 使用数据库存储

将多语言内容或关键的配置信息存储在数据库中,根据用户的语言偏好动态加载。

2.2.5 API设计结合配置中心

设计 API 时考虑国际化需求,提供语言参数,可能的话尽量支持将配置文件迁移到配置中心进行控制,从而支持多语言响应。

三、SpringBoot 国际化介绍与实践

3.1 SpringBoot 国际化概述

Spring Boot 提供了强大的国际化(i18n)支持,使得应用程序可以根据用户的语言和地区偏好显示不同的文本。通过使用 Spring 的国际化机制,你可以将应用程序的文本、日期格式、货币符号等内容进行本地化,以适应不同用户的需求。

3.1.1 SpringBoot国际化一般步骤

Spring Boot 国际化基本步骤如下:

3.2 SpringBoot 国际化实现代码演示

3.2.1 前置准备

创建一个springboot工程,并导入下面基本的依赖

<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 展示视图使用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>boot-docker</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

3.2.2 创建资源文件

创建包含不同语言版本的消息文件,这些文件通常放在 src/main/resources 目录下,并且以.properties 文件的形式的扩展名存在,比如:

如下,在resources 目录下分别创建几个对应的资源文件,各自的内容如下:

messages.properties

welcome.message=Welcome to our website!
error.message=An error occurred.

messages_en_US.properties

welcome.message=Welcome to our website!
error.message=An error occurred.

messages_zh_CN.properties

welcome.message=欢迎来到我们的网站!
error.message=发生了一个错误。

3.2.3 增加一个html模板文件

为了能够清楚看到各种语言文件展示的效果,这里我们通过一个thymeleaf目标文件将上述资源文件中的内容取出来展示到页面上显示,在resources目录下增加一个templates的目录,里面添加一个hello.html的页面,默认情况下,springboot工程会自动读取到该目录下的html文件,html文件内容如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> hello message</title>
</head>
<body>
    <h1 th:text="#{welcome.message}"></h1>
</body>
</html>

3.2.4 增加一个视图配置文件

还需在工程中增加一个配置类,从而让springboot能够解析html的模板文件,参考如下代码:

package com.congge.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/hello").setViewName("hello");
    }
}

3.2.5 效果测试与验证

启动springboot工程之后,浏览器访问:http://localhost:8081/hello ,此时将第二步中定义的资源文件内容取了出来。

为什么会展示中文这个资源文件呢?打开浏览器设置可以看到,浏览器默认使用的是中文

切换为英语之后再次访问,可以看到此时就展示为英文效果了

3.3 SpringBoot 国际化底层实现原理

3.3.1 核心配置类说明

在springboot启动加载的时候,与国际化文件相关的一个核心配置类名为 MessageSourceAutoConfiguration,源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.context;
import java.time.Duration;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
@AutoConfiguration
@ConditionalOnMissingBean(
    name = {"messageSource"},
    search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(Integer.MIN_VALUE)
@Conditional({ResourceBundleCondition.class})
@EnableConfigurationProperties
@ImportRuntimeHints({MessageSourceRuntimeHints.class})
public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];
    public MessageSourceAutoConfiguration() {
    }
    @Bean
    @ConfigurationProperties(
        prefix = "spring.messages"
    )
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }
    @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());
        }
        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }
    static class MessageSourceRuntimeHints implements RuntimeHintsRegistrar {
        MessageSourceRuntimeHints() {
        }
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources().registerPattern("messages.properties").registerPattern("messages_*.properties");
        }
    }
    protected static class ResourceBundleCondition extends SpringBootCondition {
        private static final ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap();
        protected ResourceBundleCondition() {
        }
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = (ConditionOutcome)cache.get(basename);
            if (outcome == null) {
                outcome = this.getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }
        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
            ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle", new Object[0]);
            String[] var4 = StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename));
            int var5 = var4.length;
            for(int var6 = 0; var6 < var5; ++var6) {
                String name = var4[var6];
                Resource[] var8 = this.getResources(context.getClassLoader(), name);
                int var9 = var8.length;
                for(int var10 = 0; var10 < var9; ++var10) {
                    Resource resource = var8[var10];
                    if (resource.exists()) {
                        return ConditionOutcome.match(message.found("bundle").items(new Object[]{resource}));
                    }
                }
            }
            return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
        }
        private Resource[] getResources(ClassLoader classLoader, String name) {
            String target = name.replace('.', '/');
            try {
                return (new PathMatchingResourcePatternResolver(classLoader)).getResources("classpath*:" + target + ".properties");
            } catch (Exception var5) {
                return MessageSourceAutoConfiguration.NO_RESOURCES;
            }
        }
    }
}

从这个类可以首先得到下面几个信息:

比如你不想使用默认的资源文件名称,就可以在yml文件像下面这样配置:

spring:
  messages:
    basename: language
    encoding: GBK

3.4 代码获取国际化资源配置信息

如今前后端分离的开发模式已经在各互联网公司广泛使用,在这种情况下,有时候并不需要服务端直接控制页面的语言环境,但是需要告诉页面与国际化相关的信息,通常来说,就需要服务端接口能够获取到国际化资源配置内容。

3.4.1 使用MessageSource获取资源配置信息

在spring框架中,可以通过MessageSource这个接口提供的方法获取到语言文件中的配置信息,在MessageSource接口中提供了几个获取配置信息的方法,如下:

下面提供一个接口,获取配置信息,参考下面的代码

@Resource
    private MessageSource messageSource;
    //localhost:8081/getMessage
    @GetMapping("/getMessage")
    public String getMessage(){
        String message = messageSource.getMessage("welcome.message", null, Locale.ENGLISH);
        return message;
    }

启动工程之后调用下接口,基于当前的浏览器语言环境,可以看到读取到了英文的配置信息

3.5 完整代码演示

下面结合实际项目实践经验,演示一下如何在springboot项目中集成国际化环境

3.5.1 增加配置信息

基于上述的三个resources目录下的资源配置文件,分别添加下面的信息

messages.properties

mess.user.name=玛丽  
mess.user.password=密码  
mess.user.btn=登录

messages_zh_CN.properties

mess.user.name=玛丽  
mess.user.password=密码  
mess.user.btn=登录

messages_en_US.properties

mess.user.name=merry  
mess.user.password=Password  
mess.user.btn=Sign In

3.5.2 自定义语言解析器

该类实现LocaleResolver接口,根据请求中的参数信息解析语言环境,从而匹配本地语言资源文件的内容

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import java.util.Locale;
public class SelfLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(jakarta.servlet.http.HttpServletRequest request) {
        String language = request.getHeader("lang");
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(language)) {
            String[] split = language.split("_");
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }
    @Override
    public void setLocale(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, Locale locale) {
    }
}

3.5.3 全局配置语言处理器

实现WebMvcConfigurer接口,将上述配置的解析器放到spring上下文管理的容器中

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;
@Configuration
public class LocaleConfig implements WebMvcConfigurer {
    /**
     * 默认解析器 其中locale表示默认语言,当请求中未包含语种信息,则设置默认语种
     * 当前默认为CHINA,zh_CN
     */
    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }
    /**
     * lang表示切换语言的参数名
     * 拦截请求
     *      获取请求参数lang种包含的语种信息并重新注册语种信息
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        // 前端请求头中的参数名
        lci.setParamName("lang");
        return lci;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}

3.5.4 增加i18n工具类

该类用于获取指定的资源文件中的参数值

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import java.util.Locale;
@Slf4j
@Component
public class MessageUtils {
    private static MessageSource messageSource;
    public MessageUtils(MessageSource messageSource) {
        if (log.isInfoEnabled()) {
            log.info("MessageUtils construction success");
        }
        MessageUtils.messageSource = messageSource;
    }
    /**
     * 获取单个国际化值
     */
    public static String get(String msgKey) {
        Locale locale = LocaleContextHolder.getLocale();
        try {
            return messageSource.getMessage(msgKey, null, locale);
        } catch (Exception e) {
            log.error("msgKey:{},locale:{},MessageUtils.get Exception:{}", msgKey, locale, e.getMessage());
            return messageSource.getMessage(msgKey, null, Locale.US);
        }
    }
    /**
     * 获取指定语言单个国际化的值
     */
    public static String get(String msgKey, String language) {
        try {
            //线上直接new Locale(language),会是小写en_us,调用本地方法读取资源的时候会识别不到,因为配置文件是大写的后缀en_US
            //所以这里拆分,然后拼装为大写的Locale的en_US
            String[] s = StringUtils.split(language, "_");
            Locale locale = new Locale(s[0], s[1]);
            if (log.isInfoEnabled()) {
                log.info("get,msgKey:{},language:{},locale:{}", msgKey, language, locale);
            }
            return messageSource.getMessage(msgKey, null, locale);
        } catch (Exception e) {
            log.error("msgKey:{},language:{},MessageUtils.get Exception:{}", msgKey, language, e.getMessage());
            return messageSource.getMessage(msgKey, null, Locale.US);
        }
    }
}

3.5.5 测试接口

添加如下的测试接口

//localhost:8081/getMessage/v2
    @GetMapping("/getMessage/v2")
    public String getMessageV2(){
        String val = MessageUtils.get("mess.user.name");
        return val;
    }

测试一:使用中文环境

测试二:使用英文环境

通过上面的这种方式,可以根据不同的业务需求场景,返回不同的语言环境下的配置信息,从而做到国际化适配。

3.6 其他场景补充

四、写在文末

本文详细介绍了springboot国际化的理论和解决方案,并通过案例代码演示了如何在springboot中完成国际化的通用配置,希望对看到的同学有用,本篇到此结束,感谢观看。

到此这篇关于SpringBoot 国际化适配方案使用详解的文章就介绍到这了,更多相关SpringBoot 国际化适配内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文