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实例化时注入到指定字段中。
在启动过程,除了加载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
··略··
}
由继承自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 的装配机制就是新的属性了。