Talk about ConfigClientWatch of Spring Cloud Config.

  springcloud

Order

Spring Cloud Config provides a ConfigClientWatch function, which can periodically poll the status of client configuration and refresh if the status changes.

Configuration file

spring:
  cloud:
    config:
      uri: http://localhost:8888
      watch:
        enabled: true
        initialDelay: 5000 ##default 180000 ms
        delay: 10000 ##default 500 ms

Configuration class

spring-cloud-config-client-1.3.1.RELEASE-sources.jar! /org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java

@Configuration
    @ConditionalOnClass(ContextRefresher.class)
    @ConditionalOnBean(ContextRefresher.class)
    @ConditionalOnProperty(value = "spring.cloud.config.watch.enabled")
    protected static class ConfigClientWatchConfiguration {

        @Bean
        public ConfigClientWatch configClientWatch(ContextRefresher contextRefresher) {
            return new ConfigClientWatch(contextRefresher);
        }
    }

ConfigClientWatch

public class ConfigClientWatch implements Closeable, EnvironmentAware {

    private static Log log = LogFactory
            .getLog(ConfigServicePropertySourceLocator.class);

    private final AtomicBoolean running = new AtomicBoolean(false);
    private final ContextRefresher refresher;
    private Environment environment;

    public ConfigClientWatch(ContextRefresher refresher) {
        this.refresher = refresher;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @PostConstruct
    public void start() {
        this.running.compareAndSet(false, true);
    }

    @Scheduled(initialDelayString = "${spring.cloud.config.watch.initialDelay:180000}", fixedDelayString = "${spring.cloud.config.watch.delay:500}")
    public void watchConfigServer() {
        if (this.running.get()) {
            String newState = this.environment.getProperty("config.client.state");
            String oldState = ConfigClientStateHolder.getState();

            // only refresh if state has changed
            if (stateChanged(oldState, newState)) {
                ConfigClientStateHolder.setState(newState);
                this.refresher.refresh();
            }
        }
    }

    /* for testing */ boolean stateChanged(String oldState, String newState) {
        return (!hasText(oldState) && hasText(newState))
                || (hasText(oldState) && !oldState.equals(newState));
    }

    @Override
    public void close() {
        this.running.compareAndSet(true, false);
    }

}

Rely on the environment variable of config.client.state to judge the state of the client-side configuration file.
Rely on ContextRefresher to refresh the configuration/instance

ContextRefresher

spring-cloud-context-1.2.2.RELEASE-sources.jar! /org/springframework/cloud/context/refresh/ContextRefresher.java

public synchronized Set<String> refresh() {
        Map<String, Object> before = extract(
                this.context.getEnvironment().getPropertySources());
        addConfigFilesToEnvironment();
        Set<String> keys = changes(before,
                extract(this.context.getEnvironment().getPropertySources())).keySet();
        this.context.publishEvent(new EnvironmentChangeEvent(keys));
        this.scope.refreshAll();
        return keys;
    }

This refresh mainly does two things:

  • The first is the release of the EnvironmentChangeEvent event

  • The second thing is to call the refreshAll method of RefreshScope.

Listener for EnvironmentChangeEvent

spring-cloud-context-1.2.2.RELEASE-sources.jar! /org/springframework/cloud/context/properties/ConfigurationPropertiesRebinder.java

@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
        implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {

    private ConfigurationPropertiesBeans beans;

    private ConfigurationPropertiesBindingPostProcessor binder;

    private ApplicationContext applicationContext;

    private Map<String, Exception> errors = new ConcurrentHashMap<>();

    public ConfigurationPropertiesRebinder(
            ConfigurationPropertiesBindingPostProcessor binder,
            ConfigurationPropertiesBeans beans) {
        this.binder = binder;
        this.beans = beans;
    }

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

    /**
     * A map of bean name to errors when instantiating the bean.
     *
     * @return the errors accumulated since the latest destroy
     */
    public Map<String, Exception> getErrors() {
        return this.errors;
    }

    @ManagedOperation
    public void rebind() {
        this.errors.clear();
        for (String name : this.beans.getBeanNames()) {
            rebind(name);
        }
    }

    @ManagedOperation
    public boolean rebind(String name) {
        if (!this.beans.getBeanNames().contains(name)) {
            return false;
        }
        if (this.applicationContext != null) {
            try {
                Object bean = this.applicationContext.getBean(name);
                if (AopUtils.isCglibProxy(bean)) {
                    bean = getTargetObject(bean);
                }
                this.binder.postProcessBeforeInitialization(bean, name);
                this.applicationContext.getAutowireCapableBeanFactory()
                        .initializeBean(bean, name);
                return true;
            }
            catch (RuntimeException e) {
                this.errors.put(name, e);
                throw e;
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private static <T> T getTargetObject(Object candidate) {
        try {
            if (AopUtils.isAopProxy(candidate) && (candidate instanceof Advised)) {
                return (T) ((Advised) candidate).getTargetSource().getTarget();
            }
        }
        catch (Exception ex) {
            throw new IllegalStateException("Failed to unwrap proxied object", ex);
        }
        return (T) candidate;
    }

    @ManagedAttribute
    public Set<String> getBeanNames() {
        return new HashSet<String>(this.beans.getBeanNames());
    }

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        rebind();
    }

}

RefreshAll of RefreshScope

spring-cloud-context-1.2.2.RELEASE-sources.jar! /org/springframework/cloud/context/scope/refresh/RefreshScope.java

@ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
    public void refreshAll() {
        super.destroy();
        this.context.publishEvent(new RefreshScopeRefreshedEvent());
    }

Publishing RefreshScopeRefreshedEvent Event

RefreshScopeRefreshedEvent event listener

spring-cloud-netflix-eureka-client-1.3.1.RELEASE-sources.jar! /org/springframework/cloud/netflix/eureka/EurekaDiscoveryClientConfiguration.java

@Configuration
    @ConditionalOnClass(RefreshScopeRefreshedEvent.class)
    protected static class EurekaClientConfigurationRefresher {

        @Autowired(required = false)
        private EurekaClient eurekaClient;

        @Autowired(required = false)
        private EurekaAutoServiceRegistration autoRegistration;

        @EventListener(RefreshScopeRefreshedEvent.class)
        public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
            //This will force the creation of the EurkaClient bean if not already created
            //to make sure the client will be reregistered after a refresh event
            if(eurekaClient != null) {
                eurekaClient.getApplications();
            }
            if (autoRegistration != null) {
                // register in case meta data changed
                this.autoRegistration.stop();
                this.autoRegistration.start();
            }
        }
    }

spring-cloud-netflix-core-1.2.6.RELEASE-sources.jar! /org/springframework/cloud/netflix/zuul/ZuulConfiguration.java

    @Bean
    public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
        return new ZuulRefreshListener();
    }
private static class ZuulRefreshListener
            implements ApplicationListener<ApplicationEvent> {

        @Autowired
        private ZuulHandlerMapping zuulHandlerMapping;

        private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextRefreshedEvent
                    || event instanceof RefreshScopeRefreshedEvent
                    || event instanceof RoutesRefreshedEvent) {
                this.zuulHandlerMapping.setDirty(true);
            }
            else if (event instanceof HeartbeatEvent) {
                if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
                    this.zuulHandlerMapping.setDirty(true);
                }
            }
        }

    }

Summary

Spring Cloud Config’s code provides ConfigClientWatch, but when git is used as config server, the state obtained when pulling configuration is always null, so client polling cannot achieve refresh effect. The second one is the refreshments released by the refreshments. eureka will UPdate the registration information to DOWN first and then up again. this frequent operation is somewhat risky.