美文网首页
从携程Apollo客户端源码学到的知识(一)

从携程Apollo客户端源码学到的知识(一)

作者: 稻草鸟人 | 来源:发表于2020-04-19 18:35 被阅读0次

知识点:如何执行被注解ApolloConfigChangeListener标注的方法

一、起因

事情的故事的是这个样子的,前面的文章我们自定义了一个starter①,并且集成了Apollo,可以自动从Apollo中获取配置,但是我们发现它没办法保证在Apollo修改了配置之后,同步更新到本地。我们找到官方的WIKI介绍②,我用人话重新组织了下他们的语言:我们“不支持@ConfigurationProperties自动注入,需要自己写点东西才行,具体是啥,想知道直接查看附录的超链接。

二、方案

根据附录链接,我很快定位并写了测试方法,很快....真的很快!代码如下,经过测试发现,随时修改,随时变化了,达到了我想要的效果

package com.example.fg;

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @author cattle -  稻草鸟人
 * @date 2020/4/14 下午2:01
 */
@Slf4j
@Component
public class ApolloRefreshListener implements ApplicationContextAware {

    private ApplicationContext applicationContext;


    @ApolloConfigChangeListener(value = {"application","promotion"})
    private void onChange(ConfigChangeEvent changeEvent) {
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
            log.info("change - {}", change.toString());
        }

        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

但是....事情没完!

从上面的代码我们发现@ApolloConfigChangeListener注解中的value写死了,这可愁死我了,我可不想要写死,为啥呢?这话可能扯的有点远,涉及到我对软件架构的思路,以后有机会写写。总之,我不想写死。所以我就开始了翻看apollo-client源码,这才会有了这篇文章

三、学习

1)执行过程

跟着@ApolloConfigChangeListener经过一番云雨...不对,一顿操作,让我找到了它的祖宗,大概明白了该注解到底是怎么起作用的。下面我大概说明一下apollo-client整个工程的启动顺序

ApolloApplicationContextInitializer实现了ApplicationContextInitializer接口用于初始化一些内容。

在初始化之前会先去判断apollo.bootstrap.enabled是否为true然后会获取apollo.bootstrap.namespaces配置的namespace并分别获取到所有namespaces的Config对象

for (String namespace : namespaceList) {
      Config config = ConfigService.getConfig(namespace);
            .....
}

下面我贴一下获取Config执行的代码片段

ConfigService

public static Config getConfig(String namespace) {
    return s_instance.getManager().getConfig(namespace);
  }

DefaultConfigManager

public Config getConfig(String namespace) {
    Config config = m_configs.get(namespace);

    if (config == null) {
      synchronized (this) {
        config = m_configs.get(namespace);

        if (config == null) {
          ConfigFactory factory = m_factoryManager.getFactory(namespace);

          config = factory.create(namespace);
          m_configs.put(namespace, config);
        }
      }
    }

    return config;
  }

DefaultConfigFactory

public Config create(String namespace) {
    ConfigFileFormat format = determineFileFormat(namespace);
    if (ConfigFileFormat.isPropertiesCompatible(format)) {
      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
    }
    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
  }

DefaultConfig

public DefaultConfig(String namespace, ConfigRepository configRepository) {
    m_namespace = namespace;
    m_resourceProperties = loadFromResource(m_namespace);
    m_configRepository = configRepository;
    m_configProperties = new AtomicReference<>();
    m_warnLogRateLimiter = RateLimiter.create(0.017); // 1 warning log output per minute
    initialize();
  }

  private void initialize() {
    try {
      updateConfig(m_configRepository.getConfig(), m_configRepository.getSourceType());
    } catch (Throwable ex) {
      Tracer.logError(ex);
      logger.warn("Init Apollo Local Config failed - namespace: {}, reason: {}.",
          m_namespace, ExceptionUtil.getDetailMessage(ex));
    } finally {
      //register the change listener no matter config repository is working or not
      //so that whenever config repository is recovered, config could get changed
      m_configRepository.addChangeListener(this);
    }
  }

从上面的代码我们知道ApolloApplicationContextInitializer启动的时候获取了一个或者多个DefaultConfig。DefaultConfig再初始化的时候m_configRepository.addChangeListener(this);。这段代码成功的引起的我的注意,就像一个仙女从你面前路过,就算是直男也应该有所反应,除非他在打游戏!这段代码成功的将当前DefaultConfig对象加入到了抽象类AbstractConfigRepositoryRepositoryChangeListener的集合中,

我们仔细观察一下AbstractConfigRepository的几个方法,它基本上就是一个标准的模板方法模式啊!通过调用trySync()方法执行sync(),sync()是抽象方法说明它是由子类实现的。

[图片上传失败...(image-5b8c3a-1587292509810)]

这里,我们介绍下RemoteConifgRepository。它是在什么时候初始化的呢,我们继续撸...发现DefaultConfigFactorynew DefaultConfig(namespace, createLocalConfigRepository(namespace))时createLocalConfigRepository(namespace)里面实例化一个RemoteConifgRepository

OK,到这里我们简单在总结下过程,首先项目启动的时候初始化了RemoteConifgRepository然后初始化DefaultConfig,并且将DefaultConfig加入到了RepositoryChangeListener的集合中。RemoteConfigRepository的初始化方法很有意思,我们注意到它的构造器有如下三个方法都会执行trySync(),而trySync()方法会执行sync(),sync()方法则会执行fireRepositoryChange然后执行onRepositoryChange再执行fireConfigChange最后执行ConfigChangeListeneronChange方法.....

public RemoteConfigRepository(String namespace) {
    ....
    //启动时执行一次
    this.trySync();
    //定时执行
    this.schedulePeriodicRefresh();
    //长连接拉取
    this.scheduleLongPollingRefresh();
  }

终于到正题了,到底是哪个ConfigChangeListener执行onChange方法,onChange方法的参数ConfigChangeEvent又是怎么得到的呢

2)添加ConfigChangeListener

在步骤一中,我们看到了在自定义的onChange方法上增加了一个ApolloConfigChangeListener注解,那么这个注解是如何生效并且添加ConfigChangeListener的呢

它是通过Service Provider Interface(SPI)机制,实现了一个DefaultApolloConfigRegistrarHelper,这里我们关注ApolloAnnotationProcessor类。ApolloAnnotationProcessor的父亲也就是BeanPostProcessor利用了BeanPostProcessor功能,在类初始化之前执行了自己的processMethod方法。该方法则会将自定义的listener方法添加到DefaultConfig

protected void processMethod(final Object bean, String beanName, final Method method) {
    ApolloConfigChangeListener annotation = AnnotationUtils
        .findAnnotation(method, ApolloConfigChangeListener.class);
    if (annotation == null) {
      return;
    }
....

    String[] namespaces = annotation.value();
    String[] annotatedInterestedKeys = annotation.interestedKeys();
    String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
    ConfigChangeListener configChangeListener = new ConfigChangeListener() {
      @Override
      public void onChange(ConfigChangeEvent changeEvent) {
        ReflectionUtils.invokeMethod(method, bean, changeEvent);
      }
    };
  ....

    // 添加自定义的方法到DefaultConfig中的ConfigChangeListener集合属性中
    for (String namespace : namespaces) {
      Config config = ConfigService.getConfig(namespace);

      if (interestedKeys == null && interestedKeyPrefixes == null) {
        config.addChangeListener(configChangeListener);
      } else {
        config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
      }
    }
  }

到这里我们基本了解作者的意图了,目的就是为了 执行被注解ApolloConfigChangeListener标注的自定义的方法,执行过程我们也了如指掌了。那么.....问题来了,到底怎样做ApolloConfigChangeListener上的namespace才可以不写死呢,随着项目配置自动获取所有的namespace呢?

四、附录

创建属于自己的starterhttps://mp.weixin.qq.com/s/X3kJyFHrn7tul4PiGlfT5g

Apollo指南https://github.com/ctripcorp/apollo/wiki/Java客户端使用指南#323-spring-annotation支持

一起学习吧

相关文章

网友评论

      本文标题:从携程Apollo客户端源码学到的知识(一)

      本文链接:https://www.haomeiwen.com/subject/nxcbbhtx.html