springcloud微服务国际化

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: springcloud微服务国际化

一、初探

单体应用完成国际化还是比较简单的,可以看下面的示例代码。
引入必要的依赖

<!-- SpringBoot Web -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Validator -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- i18n -->
<dependency>
   <groupId>org.webjars.bower</groupId>
   <artifactId>jquery-i18n-properties</artifactId>
   <version>1.2.7</version>
</dependency>

创建一个拦截器

import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;

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

public class LocaleInterceptor extends LocaleChangeInterceptor {
   
   
    private static final String LOCALE = "Accept-Language";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
   
   
        String newLocale = request.getHeader(LOCALE);
        if (newLocale != null) {
   
   
            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(newLocale));
            } catch (IllegalArgumentException ignore) {
   
   
            }
        }
        return true;
    }
}

创建一个配置类

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.nio.charset.StandardCharsets;
import java.util.Locale;

@Configuration
public class LocaleConfig implements WebMvcConfigurer{
   
   
    /**
     *    默认解析器 其中locale表示默认语言,当请求中未包含语种信息,则设置默认语种
     *    当前默认为简体中文,zh_CN
     */
    @Bean
    public SessionLocaleResolver localeResolver() {
   
   
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }

    /**
     *  默认拦截器
     *  拦截请求,获取请求头中包含的语种信息并重新注册语种信息
     */
    @Bean
    public WebMvcConfigurer localeInterceptor() {
   
   
        return new WebMvcConfigurer() {
   
   
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
   
   
                registry.addInterceptor(new LocaleInterceptor());
            }
        };
    }

    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
   
   
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        // 设置消息源
        bean.setValidationMessageSource(resourceBundleMessageSource());
        return bean;
    }

    @Bean
    public MessageSource resourceBundleMessageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
        // 多语言文件地址
        messageSource.addBasenames("i18n/message");
        return messageSource;
    }

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
   
   
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setValidator(localValidatorFactoryBean().getValidator());
        return processor;
    }

    @Override
    public Validator getValidator() {
   
   
        return localValidatorFactoryBean();
    }
}

然后在resource下创建i18n目录,选中右键 New =>Resource Bundle
在这里插入图片描述
填入base name,选择Project locales,再Add All,确定即可。
在这里插入图片描述
打开配置文件,填写对应的中英文数据
在这里插入图片描述
配置一下application.yml

spring:
  messages:
    basename: i18n.message
    cache-duration: 3600
    encoding: UTF-8

这样基本上就好了,使用也很简单,看下图
在这里插入图片描述

二、深入

对于微服务来讲,每个模块单独配置国际化还是很繁琐的事情。所以一般是将国际化存入数据库进行统一管理。而本地缓存使用Redis替换,从而更新国际化之后,相应的模块能同步。

先把原来的LocaleConfigLocaleInterceptor抽离到公共服务,同时增加一个自定义MessageSource

import com.xxx.common.core.domain.RpcResult;
import com.xxx.common.redis.service.RedisService;
import com.xxx.system.api.RemoteLocaleMessageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.PostConstruct;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map;

@Slf4j
@Component("messageSource")
public class CustomMessageSource extends AbstractMessageSource {
   
   
    public static final String REDIS_LOCALE_MESSAGE_KEY = "i18n_message";

    @Value("${spring.application.name}")
    private String appName;

    /**
    * 这里使用的是dubbo,也可以用open feign
    * 需要引入dubbo的依赖包 spring-cloud-starter-dubbo
    */
    @DubboReference(version = "1.0.0")
    private RemoteLocaleMessageService i18nMessageMapper;

    @Autowired
    private RedisService redisService;

    @PostConstruct
    public void init() {
   
   
        log.info("init i18n message...");
        redisService.deleteObject(REDIS_LOCALE_MESSAGE_KEY + ":" + appName);
        this.reload();
    }

    /**
     * 重新加载消息到该类的Map缓存中
     */
    public Map<String, Map<String, String>> reload() {
   
   
        Map<String, Map<String, String>> localeMsgMap = redisService.getCacheMap(REDIS_LOCALE_MESSAGE_KEY + ":" + appName);
        if (localeMsgMap == null || localeMsgMap.isEmpty()) {
   
   
            // 加载所有的国际化资源
            localeMsgMap = this.loadAllMessageResources();
            // 缓存到redis
            if (localeMsgMap != null && !localeMsgMap.isEmpty()) {
   
   
                redisService.setCacheMap(REDIS_LOCALE_MESSAGE_KEY + ":" + appName, localeMsgMap);
            }
        }
        return localeMsgMap;
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
   
   
        String msg = this.getSourceFromCacheMap(code, locale);
        return new MessageFormat(msg, locale);
    }

    @Override
    protected String resolveCodeWithoutArguments(String code, Locale locale) {
   
   
        return this.getSourceFromCacheMap(code, locale);
    }

    /**
     * 加载所有的国际化消息资源
     *
     * @return
     */
    private Map<String, Map<String, String>> loadAllMessageResources() {
   
   
        // 从数据库中查询所有的国际化资源
        RpcResult<Map<String, Map<String, String>>> rpcResult = i18nMessageMapper.getAllLocaleMessage(appName);
        return rpcResult.getCode() == 200 ? rpcResult.getData() : null;
    }

    /**
     * 缓存Map中加载国际化资源
     *
     * @param code
     * @param locale
     * @return
     */
    private String getSourceFromCacheMap(String code, Locale locale) {
   
   
        // 判断如果没有值则会去重新加载数据
        Map<String, Map<String, String>> localeMsgMap = this.reload();
        String language = ObjectUtils.isEmpty(locale) ? LocaleContextHolder.getLocale().getLanguage() : locale.getLanguage();
        // 获取缓存中对应语言的所有数据项
        Map<String, String> propMap = localeMsgMap.get(language);
        if (!ObjectUtils.isEmpty(propMap) && propMap.containsKey(code)) {
   
   
            // 如果对应语言中能匹配到数据项,那么直接返回
            return propMap.get(code);
        }
        // 如果找不到国际化消息,就直接返回code
        return code;
    }
}

并对LocaleConfig进行改造

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.nio.charset.StandardCharsets;
import java.util.Locale;

@Configuration
public class LocaleConfig implements WebMvcConfigurer {
   
   

     @Autowired
     private CustomMessageSource customMessageSource;

    /**
     *    默认解析器 其中locale表示默认语言,当请求中未包含语种信息,则设置默认语种
     *    当前默认为SIMPLIFIED_CHINESE,zh_CN
     */
    @Bean
    public SessionLocaleResolver localeResolver() {
   
   
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }

    /**
     *  默认拦截器
     *  拦截请求,获取请求头中包含的语种信息并重新注册语种信息
     */
    @Bean
    public WebMvcConfigurer localeInterceptor() {
   
   
        return new WebMvcConfigurer() {
   
   
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
   
   
                registry.addInterceptor(new LocaleInterceptor());
            }
        };
    }

    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean() {
   
   
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        bean.setMessageInterpolator(interpolatorFactory.getObject());
        // 设置消息源
        bean.setValidationMessageSource(resourceBundleMessageSource());
        return bean;
    }

    @Bean
    public MessageSource resourceBundleMessageSource() {
   
   
        return customMessageSource;
    }

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
   
   
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setValidator(localValidatorFactoryBean().getValidator());
        return processor;
    }

    @Override
    public Validator getValidator() {
   
   
        return localValidatorFactoryBean();
    }
}

在需要的模块中引入即可。

Dubbo provider的实现

import com.xxx.common.core.domain.RpcResult;
import com.xxx.system.api.RemoteLocaleMessageService;
import com.xxx.system.entity.SysLocaleMessage;
import com.xxx.system.service.ISysLocaleMessageService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@DubboService(version = "1.0.0")
public class DubboLocaleMessageService implements RemoteLocaleMessageService {
   
   
    @Autowired
    private ISysLocaleMessageService localeMessageService;

    private final String COMMON_CONFIG = "common";

    // 这里不能返回List,因为无法序列化,会导致Dubbo异常,所以使用Map
    @Override
    public RpcResult<Map<String, Map<String, String>>> getAllLocaleMessage(String module) {
   
   
        // 每个module对应的配置
        SysLocaleMessage localeMessage = new SysLocaleMessage();
        localeMessage.setModule(module);
        List<SysLocaleMessage> list = localeMessageService.queryByParam(localeMessage);
        if (list == null) {
   
   
            list = new ArrayList<>();
        }
        // 公共配置
        localeMessage = new SysLocaleMessage();
        localeMessage.setModule(COMMON_CONFIG);
        list.addAll(localeMessageService.queryByParam(localeMessage));
        if (CollectionUtils.isEmpty(list)) {
   
   
            return RpcResult.fail("no data!");
        }

        Map<String, Map<String, String>> localeMsgMap = list.stream().collect(Collectors.groupingBy(
                // 根据国家地区分组
                SysLocaleMessage::getLocale,
                // 收集为Map,key为code,msg为信息
                Collectors.toMap(SysLocaleMessage::getCode, SysLocaleMessage::getMsg)
        ));
        return RpcResult.success(localeMsgMap);
    }
}

将国际化的增删改查功能集成到系统管理,就可以通过数据库进行管理了。
image.png

对国际化进行增删改后,需要对Redis缓存进行更新

/**
 * 清理Redis所有前缀匹配的缓存
 */
private void clearCache() {
   
   
    Collection<String> keys = redisService.keys(CustomMessageSource.REDIS_LOCALE_MESSAGE_KEY + "*");
    keys.stream().forEach(key -> {
   
   
        redisService.deleteObject(key);
    });
}

/**
 * 按key清理Redis缓存
 */
private void clearCache(String key) {
   
   
    redisService.deleteObject(CustomMessageSource.REDIS_LOCALE_MESSAGE_KEY + ":" + key);
}

之前创建的resouce/i18n目录则可以删除。使用也是和单体应用一样的。
image.png

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
26天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
48 2
|
2月前
|
Java 对象存储 开发者
解析Spring Cloud与Netflix OSS:微服务架构中的左右手如何协同作战
Spring Cloud与Netflix OSS不仅是现代微服务架构中不可或缺的一部分,它们还通过不断的技术创新和社区贡献推动了整个行业的发展。无论是对于初创企业还是大型组织来说,掌握并合理运用这两套工具,都能极大地提升软件系统的灵活性、可扩展性以及整体性能。随着云计算和容器化技术的进一步普及,Spring Cloud与Netflix OSS将继续引领微服务技术的发展潮流。
51 0
|
29天前
|
Dubbo Java 应用服务中间件
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud Alibaba 微服务基础框架
尼恩团队的15大技术圣经,旨在帮助开发者系统化、体系化地掌握核心技术,提升技术实力,从而在面试和工作中脱颖而出。本文介绍了如何使用Dubbo3.0与Spring Cloud Gateway进行整合,解决传统Dubbo架构缺乏HTTP入口的问题,实现高性能的微服务网关。
|
25天前
|
JSON Java 数据格式
【微服务】SpringCloud之Feign远程调用
本文介绍了使用Feign作为HTTP客户端替代RestTemplate进行远程调用的优势及具体使用方法。Feign通过声明式接口简化了HTTP请求的发送,提高了代码的可读性和维护性。文章详细描述了Feign的搭建步骤,包括引入依赖、添加注解、编写FeignClient接口和调用代码,并提供了自定义配置的示例,如修改日志级别等。
67 1
|
29天前
|
人工智能 文字识别 Java
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
尼恩,一位拥有20年架构经验的老架构师,通过其深厚的架构功力,成功指导了一位9年经验的网易工程师转型为大模型架构师,薪资逆涨50%,年薪近80W。尼恩的指导不仅帮助这位工程师在一年内成为大模型架构师,还让他管理起了10人团队,产品成功应用于多家大中型企业。尼恩因此决定编写《LLM大模型学习圣经》系列,帮助更多人掌握大模型架构,实现职业跃迁。该系列包括《从0到1吃透Transformer技术底座》、《从0到1精通RAG架构》等,旨在系统化、体系化地讲解大模型技术,助力读者实现“offer直提”。此外,尼恩还分享了多个技术圣经,如《NIO圣经》、《Docker圣经》等,帮助读者深入理解核心技术。
SpringCloud+Python 混合微服务,如何打造AI分布式业务应用的技术底层?
|
1月前
|
监控 Java 对象存储
监控与追踪:如何利用Spring Cloud Sleuth和Netflix OSS工具进行微服务调试
监控与追踪:如何利用Spring Cloud Sleuth和Netflix OSS工具进行微服务调试
42 1
|
2月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
100 5
|
2月前
|
前端开发 API 微服务
SpringCloud微服务之间使用Feign调用不通情况举例
SpringCloud微服务之间使用Feign调用不通情况举例
471 2
|
1月前
|
负载均衡 算法 Nacos
SpringCloud 微服务nacos和eureka
SpringCloud 微服务nacos和eureka
58 0
|
2月前
|
Java 对象存储 开发者
微服务世界的双雄争霸:Spring Cloud与Netflix OSS——谁将引领下一次企业级应用变革的风暴?
Spring Cloud与Netflix OSS是微服务架构的核心组件集,分别以其与Spring Boot的紧密集成及为大规模分布式系统设计的特性,在Java开发社区中广受青睐。前者通过Eureka提供服务发现机制,简化服务注册与定位;后者借助Hystrix增强系统弹性和可靠性,避免雪崩效应。此外,二者还包含负载均衡(Ribbon)、声明式HTTP客户端(Feign)及API网关(Zuul)等功能,共同构建强大微服务体系,助力开发者聚焦业务逻辑,提升系统灵活性与性能。
46 0