spring boot 配置文件动态更新原理 以Nacos为例
Posted 二奎
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring boot 配置文件动态更新原理 以Nacos为例相关的知识,希望对你有一定的参考价值。
配置文件的动态更新
通常获取配置文件的方式
1, @Value
2. @ConfigurationProperties(Prefix)
如果是在运行时要动态更新的话,
第一种方式要在bean上加@RefreshScope
第二种方式是自动支持的。
以Nacos为为例,我们可以看下源码是如何实现的:
Nacos获取配置中心是通过单独一个线程的长轮询获取的:
com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable
当获取到更新配置后,publishEvent
org.springframework.cloud.alibaba.nacos.refresh.NacosContextRefresher#registerNacosListener
private void registerNacosListener(final String group, final String dataId) { Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() { @Override public void receiveConfigInfo(String configInfo) { refreshCountIncrement(); String md5 = ""; if (!StringUtils.isEmpty(configInfo)) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8"))) .toString(16); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { log.warn("[Nacos] unable to get md5 for dataId: " + dataId, e); } } refreshHistory.add(dataId, md5); applicationContext.publishEvent( new RefreshEvent(this, null, "Refresh Nacos config")); if (log.isDebugEnabled()) { log.debug("Refresh Nacos config group " + group + ",dataId" + dataId); } } @Override public Executor getExecutor() { return null; } }); try { configService.addListener(dataId, group, listener); } catch (NacosException e) { e.printStackTrace(); } }
当收到{@link RefreshEvent}时调用{@link RefreshEventListener#refresh}。
只在收到{@link ApplicationReadyEvent}后响应{@link RefreshEvent},
因为RefreshEvents可能在应用程序生命周期中来得太早。(译文)
public class RefreshEventListener implements SmartApplicationListener { private static Log log = LogFactory.getLog(RefreshEventListener.class); private ContextRefresher refresh; private AtomicBoolean ready = new AtomicBoolean(false); public RefreshEventListener(ContextRefresher refresh) { this.refresh = refresh; } @Override public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { return ApplicationReadyEvent.class.isAssignableFrom(eventType) || RefreshEvent.class.isAssignableFrom(eventType); } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationReadyEvent) { handle((ApplicationReadyEvent) event); } else if (event instanceof RefreshEvent) { handle((RefreshEvent) event); } } public void handle(ApplicationReadyEvent event) { this.ready.compareAndSet(false, true); } public void handle(RefreshEvent event) { if (this.ready.get()) { // don‘t handle events before app is ready log.debug("Event received " + event.getEventDesc()); Set<String> keys = this.refresh.refresh(); log.info("Refresh keys changed: " + keys); } } }
org.springframework.cloud.context.refresh.ContextRefresher
public synchronized Set<String> refresh() { Set<String> keys = refreshEnvironment(); this.scope.refreshAll(); return keys; } public synchronized Set<String> refreshEnvironment() { 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(this.context, keys)); return keys; }
ConfigurableApplicationContext addConfigFilesToEnvironment() { ConfigurableApplicationContext capture = null; try { StandardEnvironment environment = copyEnvironment( this.context.getEnvironment()); SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class) .bannerMode(Mode.OFF).web(WebApplicationType.NONE) .environment(environment); // Just the listeners that affect the environment (e.g. excluding logging // listener because it has side effects) builder.application() .setListeners(Arrays.asList(new BootstrapApplicationListener(), new ConfigFileApplicationListener())); capture = builder.run(); if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) { environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE); } MutablePropertySources target = this.context.getEnvironment() .getPropertySources(); String targetName = null; for (PropertySource<?> source : environment.getPropertySources()) { String name = source.getName(); if (target.contains(name)) { targetName = name; } if (!this.standardSources.contains(name)) { if (target.contains(name)) { target.replace(name, source); } else { if (targetName != null) { target.addAfter(targetName, source); } else { // targetName was null so we are at the start of the list target.addFirst(source); targetName = name; } } } } } finally { ConfigurableApplicationContext closeable = capture; while (closeable != null) { try { closeable.close(); } catch (Exception e) { // Ignore; } if (closeable.getParent() instanceof ConfigurableApplicationContext) { closeable = (ConfigurableApplicationContext) closeable.getParent(); } else { break; } } } return capture; }
然后就重新走了一次启动的流程
/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}. * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */ public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
public void onApplicationEvent(EnvironmentChangeEvent event) { if (this.applicationContext.equals(event.getSource()) // Backwards compatible || event.getKeys().equals(event.getSource())) { rebind(); } }
ConfigurationPropertiesRebinder 看见这个名字就知道是怎么回事了。
找到所有的ConfigurationPropertiesBeans, 遍历它们
@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.isAopProxy(bean)) { bean = ProxyUtils.getTargetObject(bean); } if (bean != null) { this.applicationContext.getAutowireCapableBeanFactory() .destroyBean(bean); this.applicationContext.getAutowireCapableBeanFactory() .initializeBean(bean, name); return true; } } catch (RuntimeException e) { this.errors.put(name, e); throw e; } catch (Exception e) { this.errors.put(name, e); throw new IllegalStateException("Cannot rebind to " + name, e); } } return false; }
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class); if (annotation != null) { bind(bean, beanName, annotation); } return bean; }
这个BeanPostProcessor里就对我们要更新的Bean进行更新最新的配置值了。如下列如红色部分
private void bind(Object bean, String beanName, ConfigurationProperties annotation) { ResolvableType type = getBeanType(bean, beanName); Validated validated = getAnnotation(bean, beanName, Validated.class); Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated } : new Annotation[] { annotation }; Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations); try { this.configurationPropertiesBinder.bind(target); } catch (Exception ex) { throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex); } }
以上是关于spring boot 配置文件动态更新原理 以Nacos为例的主要内容,如果未能解决你的问题,请参考以下文章
更新 Spring Boot 以生成 WAR 文件而不是 JAR 文件
无法使用 Spring Boot 和 Thymeleaf 以动态创建的形式获取更新的值