目录
前言
公司中使用apollo作为配置中心,最近时间比较宽裕,便决定把apollo源码学习一下,本文主要是客户端启动的相关源码。
一、apollo客户端启动完成的工作
客户端启动过程中,主要完成了读取apollo中的配置和启动基于长轮询的实时更新任务两个任务,下文主要在源码层次上解析加载apollo配置的过程,关于长轮询逻辑会在以后的文章中单独写出。
二、apollo配置加载的过程
1.引入库
apollo客户端的启动过程主要是通过ApolloApplicationContextInitializer这个类实现的,实现了EnvironmentPostProcessor和ApplicationContextInitializer接口,主要通过initialize和postProcessEnvironment两个方法继续进行,下面将分别针对两个方法进行分析。
2.initialize()
在initialize方法中又再次调用了自己的initialize(environment)方法,这个方法作为apollo的初始化方法,在postProcessEnvironment方法中也有调用,通过上面的调用链可以看到,initialize方法通过BootstrapApplicationListener调用到,当前还没有加载application.properties文件中的属性,具体原因会在后面进行分析,所以需要在系统属性或者环境变量中配置apollo.bootstrap.enable=true,才能在这生效,执行下面的initialize(environment)方法。
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
//环境中是否配置apollo.bootstrap.enabled=true,由于
String enabled = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "false");
if (!Boolean.valueOf(enabled)) {
logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
return;
}
logger.debug("Apollo bootstrap config is enabled for context {}", context);
initialize(environment);
}
3.initializ(environment)
方法源码如下所示,其中比较重要的方法是ConfigService.getConfig(namespace),该方法读取了apollo配置,并构建PropertySource,注入到容器中。
protected void initialize(ConfigurableEnvironment environment) {
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
}
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
for (String namespace : namespaceList) {
//获取apollo配置
Config config = ConfigService.getConfig(namespace);
//将属性添加到composite中
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
//将apollo属性的优先级设为最高
environment.getPropertySources().addFirst(composite);
}
getConfig方法的调用链比较长,下面是几个关键点,源码如下。
LocalFileConfigRepository createLocalConfigRepository(String namespace) {
if (m_configUtil.isInLocalMode()) {
logger.warn(
"==== Apollo is in local mode! Won't pull configs from remote server for namespace {} ! ====",
namespace);
return new LocalFileConfigRepository(namespace);
}
//先读取远程服务器配置,在读取本地缓存文件配置
return new LocalFileConfigRepository(namespace, createRemoteConfigRepository(namespace));
}
RemoteConfigRepository对象的构造方法中分别实现了远程获取配置,开启长轮询任务和定时更新任务
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
m_configCache = new AtomicReference<>();
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
m_longPollServiceDto = new AtomicReference<>();
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());
m_configNeedForceRefresh = new AtomicBoolean(true);
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),
m_configUtil.getOnErrorRetryInterval() * 8);
gson = new Gson();
//远程获取配置
this.trySync();
//开启定时更新任务
this.schedulePeriodicRefresh();
//开启长轮询任务
this.scheduleLongPollingRefresh();
}
LocalFileConfigRepository对象的构造方法中,在无法从远程获取配置的情况下,会从本地配置文件中读取配置。
public LocalFileConfigRepository(String namespace, ConfigRepository upstream) {
m_namespace = namespace;
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
this.setLocalCacheDir(findLocalCacheDir(), false);
this.setUpstreamRepository(upstream);
//从本地缓存中获取配置
this.trySync();
}
4.postProcessEnvironment()
方法的作用与initialize基本一致,不做过多解释,区别在于在哪个阶段执行。
public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {
// should always initialize system properties like app.id in the first place
initializeSystemProperty(configurableEnvironment);
//获取apollo.bootstrap.eagerLoad.enabled配置属性的值
Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);
//EnvironmentPostProcessor should not be triggered if you don't want Apollo Loading before Logging System Initialization
if (!eagerLoadEnabled) {
return;
}
//获取apollo.bootstrap.enabled配置属性的值
Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);
if (bootstrapEnabled) {
initialize(configurableEnvironment);
}
}
三、springboot配置加载的流程
springboot中配置属性的加载主要是在preperEnvironment方法中实现的,源码如下。
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
//创建环境,导入servletConfigInitParams,servletContextInitParams,systemProperties,systemEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//注入commonLine配置源,并提高有限级到最高。
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
//发布事件,完成properties文件的加载,apollo也在此处生成propertySource写入环境中,在properties文件中配置apollo.bootstrap.enabled=true
//在init方法中由于没有加而不会生效
//导入applicationConfig: [classpath:/application.properties](默认properties文件)
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
在getOrCreateEnvironment()方法中完成了对servletConfigInitParams,servletContextInitParams,systemProperties,systemEnvironment属性的加载,重点代码如下:
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
propertySources.addLast(new StubPropertySource("servletContextInitParams"));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource("jndiProperties"));
}
super.customizePropertySources(propertySources);
}
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
在listeners.environmentPrepared方法中完成了对默认文件属性的加载,主要是ConfigFileApplicationListener中的postProcessEnvironment方法实现的,重点代码如下:
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
void load() {
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
//加载用户配置的文件
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
//此处加载默认配置"classpath:/,classpath:/config/,file:./,file:./config/";
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
}
其中需要重点关注initializeProfiles()方法,该方法实现了不同配置环境的切换,通过配置spring.profiles.active的值能够指定不同的环境。这里其实我是有些疑惑的,我们可以在properties默认文件中添加spring.profiles.active属性也能够生效,但是active文件的属性读取时机似乎在默认文件之前,在默认文件加载的时候会对其中的spring.profiles.active和spring.profiles.include属性过滤,添加相关文件。
private void initializeProfiles() {
// The default profile for these purposes is represented as null. We add it
// first so that it is processed first and has lowest priority.
this.profiles.add(null);
//spring.profiles.active属性对应的文件,可用于切换不同环境,获取"spring.profiles.active"对应的值
Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
//spring.profiles.include属性对应的文件
Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
this.profiles.addAll(otherActiveProfiles);
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
this.profiles.addAll(includedViaProperty);
addActiveProfiles(activatedViaProperty);
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
active文件只会添加一次。
void addActiveProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
//active文件只有一个会生效
if (this.activatedProfiles) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Profiles already activated, '" + profiles + "' will not be applied");
}
return;
}
this.activatedProfiles = true;
}
这里有一个问题,我们的一般配置是spring.profiles.active=dev,那么dev是如何对应application-dev.properties文件的。
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
//文件名拼接 prefix=classpath:/application,profile=dev,fileExtension=.properties
//profileSpecificFile=classpath:/application-dev.properties
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// Also try the profile-specific section (if any) of the normal file
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
四、springboot和apollo属性加载的先后顺序
在大概流程是先加载springboot的系统属性和环境属性,然后执行ApolloApplicationContextInitializer的initialize方法,再加载springboot的默认properties属性,再执行ApolloApplicationContextInitializer的postProcessEnvironment方法,下面给出关键代码。
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//调用环境中的applicationListeners
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
//调用postProcessEnvironment方法,实现默认属性和apollo属性加载
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
总结
本文分析了apollo客户端的启动流程,并对apollo属性和springboot原有属性的加载顺序进行分析,如有遗漏之处。还请大家指出。