nacos如何实现配置加载与动态刷新配置

nacos如何实现动态刷新配置

1、nacos启动流程

前置知识:

1、springBoot在启动过程中,大致流程为首先从classpath/META-INF/spring.facotries文件中加载并实例化initializers和listeners,会在prepareContext时通过执行这些initializer操作applicationContext

2、SprtingApplication有一个十分重要的属性。primarySorces。整个springBoot启动过程都围绕着它,正常情况下primarySorce在启动入口就被指定了。例如

    // primarySorce即为UserWebStarterApplication
     SpringApplication.run(UserWebStarterApplication.class, args);

如不指定basePackage,springboot会以 primarySorce为basePackage扫描类加载进bean工厂,如果有注解@EnableAutoConfiguration,会从classpath/META-INF/spring.facotries中将所有configuration加载到bean工厂。

3、配置文件的加载,创建Environment时,springboot会加载配置文件,默认文件名为application。在触发environmentPrepared事件时,EnvironmentPostProcessorApplicationListener.onApplicationEnvironmentPreparedEvent,接下来会执行org.springframework.boot.context.config.ConfigDataImporter#resolveAndLoad

            //根据默认application分别匹配不同的路径,不同的后缀,获得所有能加载的配置文件
			List resolved = resolve(locationResolverContext, profiles, locations);
            //配置文件加载
			return load(loaderContext, resolved);

在匹配配置文件过程中,有个逻辑,未指定就是application。

private String[] getConfigNames(Binder binder) {
    //如果spring.config.name有值,则取,否则为默认application
		String[] configNames = binder.bind(CONFIG_NAME_PROPERTY, String[].class).orElse(DEFAULT_CONFIG_NAMES);
		return configNames;
	}

配置文件被加载后将会在bean实例化时注入到指定字段中。

加载bootstrap配置文件

在启动过程,除了加载application文件外还加载名为bootstrap的配置文件。

正常启动流程,会创建一个applicationContext即一个bean工厂,启动流程中加载application或者指定配置文件。那么如何在加载application配置文件的基础上再额外加载bootstrap,或者nacos上的配置文件呢?

加载bootstrap配置文件
启动时当执行到prepareContext触发environmentPrepared事件,进入org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent,然后进入org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext

	StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
        //将默认Environment的PropertySource全部清空
		MutablePropertySources bootstrapProperties = bootstrapEnvironment
				.getPropertySources();
		for (PropertySource source : bootstrapProperties) {
			bootstrapProperties.remove(source.getName());
		}
		String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
		Map bootstrapMap = new HashMap<>();
        //将配置文件名指定为bootstrap
		bootstrapMap.put("spring.config.name", configName);
        ····省略代码···
        	SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles())
                //不打印启动banner
                .bannerMode(Mode.OFF)
                //设置新创建的Environment
				.environment(bootstrapEnvironment)
				// Don't use the default properties in this builder
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
         //设置primarySource为 BootstrapImportSelectorConfiguration
        builder.sources(BootstrapImportSelectorConfiguration.class);

        //又新创建了一个springCloud的springAplication 
        final SpringApplication builderApplication = builder.application();
        //启动这个新创建的applicationContext
        ConfigurableApplicationContext context = builder.run();

        //设置祖先Initializer 后续执行时会把新创建的applicationContext中的配置加入到实际的applicationContext
        addAncestorInitializer(application, context);

通过上面代码分析BootstrapApplicationListener新创建的springCloud的springApplication与主应用程序启动创建的application的
区别:

1、primarySource不同,扫描的basePackage不同

2、不会打印banner

3、加载的配置文件名称不同,新创建的加载配置文件名为bootstrap

相同点:

和启动时创建的springBoot相同,首先也从classpath/META-INF/spring.facotries文件中加载并实例化initializers和listeners,会在prepareContext时通过执行这些initializer操作applicationContext

新创建的springApplication加载bootstrap中的配置文件并且围绕BootstrapImportSelectorConfiguration进行bean的加载

通过BootstrapApplicationListener创建的springCloud的springApplication中创建的applicationContext即bean工厂,,经过ApplicationContextInitializer处理,会成为主应用程序启动创建的application的parent,在尝试从bean工厂中获取bean时,如本工厂不存在,从parent工厂中获取。

	BeanFactory parentBeanFactory = getParentBeanFactory();
	if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
		··略··
	return parentBeanFactory.getBean
	    ··略··
	}

启动拉取nacos上配置

由继承自ApplicationContextInitializer PropertySourceBootstrapConfiguration在prepareContext.applyInitializers时,执行initialize,最终来到com.alibaba.nacos.client.config.NacosConfigService#getConfigInner开始拉取配置,根据,dataId,dataGroup,
通过http接口/v1/cs/configs,访问nacos服务器获取到配置文件内容。解析后加入到environment中的propertySources,这样程序就可以使用这些变量了

后续springBoot正常初始化,就可以使用nacos上的配置了,

即时刷新配置的实现

1、http长轮询
com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable中不断请求服务端查看配置文件是否有变化,nacos客户端在发送http请求之前,设置http请求头

  //timeout默认30秒
 headers.put("Long-Pulling-Timeout", "" + timeout);

nacos服务端

  public void listener(HttpServletRequest request, HttpServletResponse response)
  ···省略代码···
        inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
  ···省略代码···		
    }

	public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map clientMd5Map,
            int probeRequestSize) {
        
		//获取客户端设置的timeout
        String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
    
        // 在客户端超时时间的基础上减去500ms,防止客户端超时
        long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
      
	    //开启http异步请求,可以不在当前线程给出本次请求响应
        final AsyncContext asyncContext = req.startAsync();
        
        scheduler.execute(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

    

在ClientLongPolling中,将客户端信息放入allSubs中,同时启动定时器,29.5秒后回复客户端http请求。

nacos服务端文件变更时触发变更事件

public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,{
	   persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, true);
                ConfigChangePublisher
                        .notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
            
}

触发ConfigDataChangeEvent事件后,nacos会向集群所有节点发送dump请求,各个节点将接收到的变化内容进行dump操作,如果对比变化内容和缓存的内容不同,触发LocalDataChangeEvent事件,然后触发
com.alibaba.nacos.config.server.service.LongPollingService#onEvent方法进入执行DataChangeTask的方法

            for (Iterator iter = allSubs.iterator(); iter.hasNext(); ) {
				    if (clientSub.clientMd5Map.containsKey(groupKey)) {
						     clientSub.sendResponse(Arrays.asList(groupKey));
			}

还记得allsub吗,里面记录了所有订阅者,从中挑出与变更有关的订阅者,通知他们配置文件已经变更了。同时取消之前启动的29.5秒的定时器。

总结:nacos服务器收到客户端请求监听配置的请求后。开启异步请求,并且将客户端加入到订阅队列中。开启任务最迟29.5秒后回复。如果期间服务器配置变化,nacos会根据订阅队列提前回复客户端,同时取消29.5秒的定时任务。

nacos客户端接收到服务端的回复后,客户端解析配置并注入到对应实例中。

客户端接收到变更配置文件后发布事件

applicationContext.publishEvent(
								new RefreshEvent(this, null, "Refresh Nacos config"));

最终进入org.springframework.cloud.endpoint.event.RefreshEventListener进行将配置文件刷新进入environment中的操作。

ConfigurableApplicationContext addConfigFilesToEnvironment() {
	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();
 }

这次新创建的spingApplication的primarySource被设置成了org.springframework.cloud.context.refresh.ContextRefresher.Empty,ervoironment设置设置为当前配置的复制品,同时设置两个监听器。

    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()));

着重看这两个监听器的作用。执行以Empty为primarySource的springApplication时,这里同样会触发BootstrapApplicationListener,跟之前的作用相同它又会创建一个springApplication,重复之前的操作,首先从以BootstrapImportSelectorConfiguration为primarySource,并加载获取bootstrap中的配置文件。
然后继续执行以Empty为primarySource的springApplication。
在中prepareContext,最终和启动时相同在com.alibaba.nacos.client.config.NacosConfigService#getConfig从nacos中拉取配置。最终也会放入解析后加入到environment中的propertySources。
最总以Empty为primarySource的springApplication加载完毕返回,
真正ConfigurableApplicationContext的getEnvironment中

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;
						}
					}
				}
			}
		}

那么问题来了。项目启动时bean已经初始化并且都且启动时的配置,放在了bean工厂,那么怎么把配置重新注入到对应的bean中呢?

@RefreshScope的作用

关键类RefreshScope和其继承的父类GenericScope,GenericScope继承自BeanDefinitionRegistryPostProcessor,在spring容器启动时会执行postProcessBeanDefinitionRegistry。并且可以修改所有的的bean定义。


				root.setBeanClas(LockedScopedProxyFactoryBean.class);
				root.getConstructorArgumentValues

在org.springframework.context.annotation.AnnotationScopeMetadataResolver#resolveScopeMetadata中,会将setScopedProxyMode设置成为TARGET_CLASS

	AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
			AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
					annDef.getMetadata(), this.scopeAnnotationType);
			if (attributes != null) {
				metadata.setScopeName(attributes.getString("value"));
				ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
				if (proxyMode == ScopedProxyMode.DEFAULT) {
					proxyMode = this.defaultProxyMode;
				}
				metadata.setScopedProxyMode(proxyMode);

@RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是在创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象

对于@Scope为refresh的bean,在实例化时不通过bean工厂实例化而是通过RefreshScope进行实例化

Object scopedInstance = scope.get(beanName, () -> {
							beforePrototypeCreation(beanName);
							try {
								return createBean(beanName, mbd, args);
							}
							finally {
								afterPrototypeCreation(beanName);
							}
						});
						bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);

在GenericScope中维护了一个BeanLifecycleWrapperCache,其包裹了所有@Scope为refresh的bean实例,
bean缓存在BeanLifecycleWrapperCache中。

继续前面讲到的如果nacos客户端检测到了配置文件发生变化,会重新创建新的容器加载配置,同时还会发布事件

	applicationContext.publishEvent(
								new RefreshEvent(this, null, "Refresh Nacos config"));
						

事件最终触发了org.springframework.cloud.context.refresh.ContextRefresher#refresh,会把GenericScope中的缓存BeanLifecycleWrapperCache清空。

在下一次使用对象的时候,由于代理的原因,会调用GenericScope get(String name, ObjectFactory objectFactory) 方法创建一个新的对象,并存入缓存中,此时新对象因为Spring 的装配机制就是新的属性了。

你可能感兴趣的:(spring,spring,boot,java)