前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过Apollo和nacos的能力进行国际化热更新

通过Apollo和nacos的能力进行国际化热更新

作者头像
简熵
发布2023-03-06 21:48:19
6540
发布2023-03-06 21:48:19
举报
文章被收录于专栏:逆熵逆熵

1.apollo的自动刷新

Apollo(阿波罗)是一款可靠的分布式配置管理中心,有了它,我们可以用来做很多事情:配置的热更新,配置监听,灰度发布,微服务的多环境配置隔离等。项目中引入apollo-client,就可以做到以上功能。

maven依赖:

代码语言:javascript
复制
<dependency>
     <groupId>com.ctrip.framework.apollo</groupId>
     <artifactId>apollo-client</artifactId>
     <version>1.8.0</version>
 </dependency>

通过注解@ApolloConfigChangeListener定义监听逻辑,可以指定监听的namespace,可以指定感兴趣的key。就像是事件机制一样,当一个事件感兴趣的事件过来的时候,我们可以监听并处理一些特殊的操作。

定义一个配置类并且注入到spring容器中就生效了。

代码语言:javascript
复制
public class ApolloListenerConfig {
     @ApolloConfigChangeListener({"test-i18n_zh_CN"})
     public void propertiesChanged(ConfigChangeEvent changeEvent) {
       log.info("changeEvent namespace {}", changeEvent.getNamespace());
       Set<String> set = changeEvent.changedKeys();
       set.forEach(key -> {
         ConfigChange change = changeEvent.getChange(key);
         String newValue = change.getNewValue();
         if (change.getChangeType() == PropertyChangeType.DELETED) {
           properties.remove(key);
        } else if (change.getChangeType() == PropertyChangeType.MODIFIED) {
           properties.setProperty(key, newValue);
        } else {
           properties.put(key, newValue);
        }
      });
    }
 }

返回值:changeEvent namespace test-i18n_zh_CN,测试成功,后续我们会使用这个监听机制来实现本地国际化配置的刷新。其中对于每一个key都可以有增删改的事件,以方便我们进行特殊的处理。

2.借助apollo实现语言配置的界面化

接下来就是借助apollo的配置界面来实现我们的语言配置的界面化。我们会在项目对应的apollo配置中新增一个新的namespace:test-i18n_zh_CN,它会对应我们本地的国际化配置文件test-i18n_zh_CN.properties

在项目中resource目录下创建test-i18n_zh_CN.properties,其中内容为:

代码语言:javascript
复制
 welcome=你好,世界!

我们可以随意的使用apollo的配置界面进行配置的增删改,并且还可以进行回滚,历史审计,环境隔离,从而保障配置的动态远程配置,隔离性,可审计。

至此,主要是把apollo的一些界面操作做了一些铺垫,我们在界面的操作,客户端会准实时的感知到,并且基于上文提到的监听,我们能按需进行一些操作。

上一篇文章 从源码看Spring的i18n·优雅的国际化实战,我们对spring的I18n的组件MessageSource进行了源码剖析,并对比了他的三个实现。接下来,我们谈谈怎么如何结合apollo的能力做到语言配置的动态刷新。

3.Apollo&i18n的动态刷新

3.1 纯编码内存·方案

上一篇文章 从源码看Spring的i18n·优雅的国际化实战一文也提到过StaticMessageSource可以通过编码的形式来自定义配置源。从这一点出发,我们可以借助apollo去维护内存中的配置信息。

首先,自定义MineApolloStaticMessageSource,定义语言配置的初始化。

代码语言:javascript
复制
@Component
 @Slf4j
 public class MineApolloStaticMessageSource extends  StaticMessageSource implements InitializingBean {
     private List<String> namespaces= Arrays.asList("test-i18n_zh_CN","test-i18n_en_UK");
         @Override
         public void afterPropertiesSet() throws Exception {
           for (String namespace : namespaces) {
             final Config config = ConfigService.getConfig(namespace);
             final Set<String> keys = config.getPropertyNames();
             for (String key : keys) {
               final String value = config.getProperty(key, "");
               if (StringUtils.isNotBlank(value)){
                 final String[] locate = namespace.split("_");
                 addMessage(key,new Locale(locate[1],locate[2]),value);
              }
            }
          }
     
     }
 }

然后,定义监听配置更新,刷新内存中的国际化配置。

代码语言:javascript
复制
@ApolloConfigChangeListener({"test-i18n_zh_CN","test-i18n_en_UK"})
 public void propertiesChanged(ConfigChangeEvent changeEvent) {
     try {
     final String namespace = changeEvent.getNamespace();
 
     Set<String> set = changeEvent.changedKeys();
     set.forEach(key -> {
         ConfigChange change = changeEvent.getChange(key);
         String newValue = change.getNewValue();
         final String[] locate = namespace.split("_");
         if (change.getChangeType() == PropertyChangeType.DELETED) {
         messageSource.addMessage(key,new Locale(locate[1],locate[2]),"");
        }else {
         messageSource.addMessage(key,new Locale(locate[1],locate[2]),newValue);
        }
 
         log.info(" namespace {} key {} newValue {}",namespace,key,newValue);
    });
    } catch (Exception e) {
    log.warn("", e);
    }
 }

apollo配置界面中我们定义了俩个namespace,分别是"test-i18n_zh_CN","test-i18n_en_UK",并且用于测试已经添加了一些key。

项目启动之后,我们做个简单的测试。

代码语言:javascript
复制
Locale zhLocale=new Locale("zh","CN");
Locale enLocale=newLocale("en","UK");
final StringzhMessage = apolloStaticMessageSource.getMessage(code, null, zhLocale);
 
final StringenMessage = apolloStaticMessageSource.getMessage(code, null, enLocale);
log.info("位于中国,消息为 {}",zhMessage);
log.info("in UK,message {}",enMessage);

查看日志,可以看到俩条输出:

位于中国,消息为 你好,世界!! in UK,message hello world!

3.2 结合静态配置文件·方案

MessageResource还有一个实现ReloadableResourceBundleMessageSource,通过监听本地文件从而能够在指定时间内刷新国际语言的配置。在这里我们也来实现一下这个方案。

首先,先创建ReloadableResourceBundleMessageSource实例,

并注入到spring容器中,我们可以定义语言配置的存储目录,缓存的刷新检查间隔,设置语言编码等。

代码语言:javascript
复制
@Bean
 public ReloadableResourceBundleMessageSource i18nMessageSource() {
   ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
   messageSource.setBasename("classpath:i18n/message");
   messageSource.setCacheSeconds(120);
   messageSource.setUseCodeAsDefaultMessage(true);
   messageSource.setDefaultEncoding("utf-8");
   log.info("*** i18n message source loaded ***");
   return messageSource;
 }

在resource目录下,有俩个配置文件,

"test-i18n_zh_CN.properties" "test-i18n_en_UK.properties"

apollo配置界面中我们定义了俩个namespace,分别是"test-i18n_zh_CN","test-i18n_en_UK",和上文图片一致。

最大的不同是对于监听事件的处理需要定制化,基于ReloadableResourceBundleMessageSource的刷新静态文件机制来实现国际化语言配置的刷新。

代码语言:javascript
复制
@ApolloConfigChangeListener({"test-i18n_zh_CN","test-i18n_en_UK"})
 public void propertiesChanged2(ConfigChangeEvent changeEvent) {
   try {
     String resourceName = changeEvent.getNamespace() + ".properties";
     log.info("resourceName:{}", resourceName);
     final File file = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath() + "/" + resourceName);
     Properties properties = new Properties();
     properties.load(new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8")));
 
     Set<String> set = changeEvent.changedKeys();
     set.forEach(key -> {
       ConfigChange change = changeEvent.getChange(key);
       String newValue = change.getNewValue();
       if (change.getChangeType() == PropertyChangeType.DELETED) {
         properties.remove(key);
      } else if (change.getChangeType() == PropertyChangeType.MODIFIED) {
         properties.setProperty(key, newValue);
      } else {
         properties.put(key, newValue);
      }
    });
 
     OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
     properties.store(writer, "properties reload");
  } catch (Exception e) {
     log.warn("properties reload error", e);
  }
 }

项目启动之后,我们做个简单的测试。

代码语言:javascript
复制
Locale zhLocale=new Locale("zh","CN");
Locale enLocale=new Locale("en","UK");
final String zhMessage = apolloStaticMessageSource.getMessage(code, null, zhLocale);

final String enMessage = apolloStaticMessageSource.getMessage(code, null, enLocale);
log.info("位于中国,消息为 {}",zhMessage);
log.info("in UK,message {}",enMessage);

修改apollo的配置,可以发现message和apollo一致,动态配置生效。查看日志,可以看到俩条输出:

位于中国,消息为 three.body in UK,message Dark Forest Rule

同时,我们也用到了ReloadableResourceBundleMessageSource定时缓存刷新机制,在apollo更新之后,120秒之后,才会感知新的配置,读取新的message。

3.3 优化点·初始刷新静态文件

在测试的过程中,发现这种方式下存在一个小问题,就是apollo不触发刷新,本地的静态语言配置和apollo是不一致的,就算我们服务发布上线了,也只有本地文件中的默认值,这样就出问题了。所以我们需要在项目启动时去初始更新apollo中的配置到本地文件:

代码语言:javascript
复制
@Override
public void afterPropertiesSet() throws Exception {
  for (String namespace : namespaces) {
    final Config config = ConfigService.getConfig(namespace);
    final Set<String> keys = config.getPropertyNames();
    Properties properties = new Properties();
    String resourceName = namespace + ".properties";
    log.info("resourceName:{}", resourceName);
    final File file = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath() + "/" + resourceName);
    for (String key : keys) {
      final String value = config.getProperty(key, "");
      if (StringUtils.isNotBlank(value)){
        properties.setProperty(key,value);
      }
    }
    OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
    properties.store(writer, "properties reload");
  }

}

经过测试,在项目启动之后,本地文件的内容会被apollo刷新,同时定时缓存刷新机制也ok,这样子就可以基于以上的方案进行国际化动态配置了。

4.基于Nacos的热更新

基于ReloadableResourceBundleMessageSource+nacos同样可以做到上文的效果,原理都是想通的,我们借助nacos的配置界面配置多个语言配置,其实本质来说,他们就映射本地语言配置的properties。

然后就是改造ReloadableResourceBundleMessageSource的方法,改造读取properties的方法,只需要将从本地文件读取变成改成从nacos中读取即可。

代码语言:javascript
复制
final Properties props = newProperties();
final String dataId = filename + NacosConstants.PROPERTIES_SUFFIX;
logger.info("Loading properties for " + dataId);
final String config = nacosConfigManager.getConfigService().getConfig(dataId, nacosGroup, 3000);

还有就是改造refreshProperties刷新的流程,我们通过nacos的监听机制,获取到刷新事件之后,就可以直接刷新本地缓存中的properties。

本质上和上文一样,更新的是缓存中的properties。

代码语言:javascript
复制
ConfigService configService = nacosConfigManager.getConfigService();
configService.addListener("fileName.properties", "group", new Listener() {
    @Override
    public Executor getExecutor() {
        return executor;
    }
    @Override
    public void receiveConfigInfo(String configInfo) {
            //刷新缓存的properties,省略
    }
});

已上,就是基于nacos的刷新方案。此方案省略了本地的静态文件,依赖于nacos的配置拉取。

其实,apollo也可以这样子的思路去实现。就只论配置中心来说,相比于nacos,apollo的环境隔离性,审计能力,权限控制更胜一筹。nacos想要有这些细粒度的功能就得上付费版的MSE套件了。

国际化方案的实现有很多,还需要前端的配合,比如Vue也有国际化方案,正在用于生产环境的方案需要好好推敲,适合自己业务的方案才是好的方案。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-02-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 逆熵架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.apollo的自动刷新
  • 2.借助apollo实现语言配置的界面化
  • 3.Apollo&i18n的动态刷新
  • 4.基于Nacos的热更新
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档