一、基本原理
我们知道SpringMVC Web容器启动的时候会读取配置文件来进行Envirement的初始化设值,再根据Envirement中的信息去进行Web容器的初始化启动。spring_cloud_config就是对这里的拓展,其统一管理这些配置文件,然后其他的服务在启动的时候通过http请求去config_server读取本服务的配置环境。在这里,config_server就根据请求的相关信息如服务名称&环境信息&分支去读取其管理的配置文件,再将配置文件中的信息封装为一个Envirement返回给调用方。这就是config_server主要处理的逻辑,下面我们就来具体分析这个否则。
二、远程调用介绍
1、调用方
1)、PropertySourceBootstrapConfiguration
调用方在去config_server读取Envirement的发起方法是PropertySourceBootstrapConfiguration
@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
可以看到其是实现的ApplicationContextInitializer
接口。下面我们来看其的方法调用:
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
CompositePropertySource composite = new CompositePropertySource(
BOOTSTRAP_PROPERTY_SOURCE_NAME);
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
source = locator.locate(environment);
if (source == null) {
continue;
}
......
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
......
insertPropertySources(propertySources, composite);
......
}
}
这里的逻辑主要是有两个:通过PropertySourceLocator
去远程获取对应的PropertySource
(其是从远程获取的Envirement提取的),然后再通过insertPropertySources
方法将其添加到本服务的Envirement中MutablePropertySources propertySources = environment.getPropertySources()
。
private void insertPropertySources(MutablePropertySources propertySources,
CompositePropertySource composite) {
MutablePropertySources incoming = new MutablePropertySources();
incoming.addFirst(composite);
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
Binder.get(environment(incoming)).bind("spring.cloud.config", Bindable.ofInstance(remoteProperties));
if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
&& remoteProperties.isOverrideSystemProperties())) {
propertySources.addFirst(composite);
return;
}
if (remoteProperties.isOverrideNone()) {
propertySources.addLast(composite);
return;
}
if (propertySources
.contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
if (!remoteProperties.isOverrideSystemProperties()) {
propertySources.addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
composite);
}
else {
propertySources.addBefore(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
composite);
}
}
else {
propertySources.addLast(composite);
}
}
可以看到这里还有决定是添加在addLast
或addBefore
。
2)、ConfigServicePropertySourceLocator
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
......
// Try all the labels until one works
for (String label : labels) {
Environment result = getRemoteEnvironment(restTemplate, properties,
label.trim(), state);
if (result != null) {
......
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source
.getSource();
composite.addPropertySource(
new MapPropertySource(source.getName(), map));
}
}
......
return composite;
}
}
}
......
}
上面的this.propertySourceLocators
其中就是ConfigServicePropertySourceLocator
,这里就是通过getRemoteEnvironment
方法去获取config_server中的信息,在获取其的result.getPropertySources()
信息。
private Environment getRemoteEnvironment(RestTemplate restTemplate,
ConfigClientProperties properties, String label, String state) {
String path = "/{name}/{profile}";
String name = properties.getName();
String profile = properties.getProfile();
String token = properties.getToken();
int noOfUrls = properties.getUri().length;
if (noOfUrls > 1) {
logger.info("Multiple Config Server Urls found listed.");
}
Object[] args = new String[] { name, profile };
if (StringUtils.hasText(label)) {
if (label.contains("/")) {
label = label.replace("/", "(_)");
}
args = new String[] { name, profile, label };
path = path + "/{label}";
}
ResponseEntity<Environment> response = null;
for (int i = 0; i < noOfUrls; i++) {
Credentials credentials = properties.getCredentials(i);
String uri = credentials.getUri();
String username = credentials.getUsername();
String password = credentials.getPassword();
logger.info("Fetching config from server at : " + uri);
try {
HttpHeaders headers = new HttpHeaders();
addAuthorizationToken(properties, headers, username, password);
if (StringUtils.hasText(token)) {
headers.add(TOKEN_HEADER, token);
}
if (StringUtils.hasText(state) && properties.isSendState()) {
headers.add(STATE_HEADER, state);
}
final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
Environment.class, args);
......
Environment result = response.getBody();
return result;
}
return null;
}
这里就是远程调用config_server的方法。
2、被调用方(config_server)
1)、配置文件配置的信息
spring:
cloud:
config:
#对应分支
# label: master
profile: dev
name: config-client
这里的spring.cloud.config.name
- 对应的应用名称、spring.cloud.config.profile` - 对应的环境名称、
spring.cloud.config.label` - 对应的分支名称(注释了)。
1)、EnvironmentController
@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
config_server就是用这个类去接收调用方获取Envirement请求的。这个类是有很多接收的方法:
@RequestMapping("/{name}/{profiles:.*[^-].*}")
public Environment defaultLabel(@PathVariable String name,
@PathVariable String profiles) {
return labelled(name, profiles, null);
}
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
......
return environment;
}
@RequestMapping("/{name}-{profiles}.properties")
public ResponseEntity<String> properties(@PathVariable String name,
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
return labelledProperties(name, profiles, null, resolvePlaceholders);
}
@RequestMapping("/{label}/{name}-{profiles}.properties")
public ResponseEntity<String> labelledProperties(@PathVariable String name,
@PathVariable String profiles, @PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws IOException {
......
return getSuccess(propertiesString);
}
@RequestMapping("{name}-{profiles}.json")
public ResponseEntity<String> jsonProperties(@PathVariable String name,
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
return labelledJsonProperties(name, profiles, null, resolvePlaceholders);
}
@RequestMapping("/{label}/{name}-{profiles}.json")
public ResponseEntity<String> labelledJsonProperties(@PathVariable String name,
@PathVariable String profiles, @PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
......
return getSuccess(json, MediaType.APPLICATION_JSON);
}
@RequestMapping({ "/{name}-{profiles}.yml", "/{name}-{profiles}.yaml" })
public ResponseEntity<String> yaml(@PathVariable String name,
@PathVariable String profiles,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
return labelledYaml(name, profiles, null, resolvePlaceholders);
}
@RequestMapping({ "/{label}/{name}-{profiles}.yml",
"/{label}/{name}-{profiles}.yaml" })
public ResponseEntity<String> labelledYaml(@PathVariable String name,
@PathVariable String profiles, @PathVariable String label,
@RequestParam(defaultValue = "true") boolean resolvePlaceholders)
throws Exception {
validateProfiles(profiles);
......
return getSuccess(yaml);
}
我们本次的请求是会进入第一个@RequestMapping("/{name}/{profiles:.*[^-].*}")
,其调用的是第二个方法,这个方法就是我们本次的主要分析内容了。
三、config_server读取解析返回Envirement
下面我们就来具体分析下labelled(@PathVariable String name, @PathVariable String profiles,@PathVariable String label)
方法。
1、labelled(…)方法 (EnvironmentController)
@RequestMapping("/{name}/{profiles}/{label:.*}")
public Environment labelled(@PathVariable String name, @PathVariable String profiles,
@PathVariable String label) {
if (name != null && name.contains("(_)")) {
// "(_)" is uncommon in a git repo name, but "/" cannot be matched
// by Spring MVC
name = name.replace("(_)", "/");
}
if (label != null && label.contains("(_)")) {
// "(_)" is uncommon in a git branch name, but "/" cannot be matched
// by Spring MVC
label = label.replace("(_)", "/");
}
Environment environment = this.repository.findOne(name, profiles, label);
if(!acceptEmpty && (environment == null || environment.getPropertySources().isEmpty())){
throw new EnvironmentNotFoundException("Profile Not found");
}
return environment;
}
1)、(_)替换符处理
可以看到这里有一种操作将(_)
转换为/
,这里其实就一种转换,因为如果将/
放在请求上是代表的另一个请求,所以这里是有一个替换,这个替换时请求获取的时候可内容请求本身还有一层路径:
@Test
public void applicationPlaceholderWithSlashForBinary() throws Exception {
this.environmentRepository.setSearchLocations("classpath:/test/{application}");
byte[] resource = this.controller.binary("dev(_)spam", "bar", "", "foo.txt");
assertEquals("foo: dev_bar/spam", new String(resource));
}
例如这里其表示要获取的地址是dev/spam
,可以看到也可以在搜索地址classpath:/test/{application}
中写{application}
这种替换符。
2)、this.repository.findOne(…)
private EnvironmentRepository repository;
Environment environment = this.repository.findOne(name, profiles, label);
这个就是环境仓库接口:
这里我们可以看到JGitEnvironmentRepository
这个是用来处理Git仓库的,而我们本次是读取本地文件,其用的是NativeEnvironmentRepository
。不过这里是用来代理模式:
public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRepository {
@Override
public Environment findOne(String name, String profiles, String label) {
Environment environment = this.delegate.findOne(name, profiles, label);
if (this.environmentEncryptor != null) {
environment = this.environmentEncryptor.decrypt(environment);
}
if (!this.overrides.isEmpty()) {
environment.addFirst(new PropertySource("overrides", this.overrides));
}
return environment;
}
public class CompositeEnvironmentRepository implements EnvironmentRepository {
protected List<EnvironmentRepository> environmentRepositories;
@Override
public Environment findOne(String application, String profile, String label) {
Environment env = new Environment(application, new String[]{profile}, label, null, null);
if(environmentRepositories.size() == 1) {
Environment envRepo = environmentRepositories.get(0).findOne(application, profile, label);
env.addAll(envRepo.getPropertySources());
env.setVersion(envRepo.getVersion());
env.setState(envRepo.getState());
} else {
for (EnvironmentRepository repo : environmentRepositories) {
env.addAll(repo.findOne(application, profile, label).getPropertySources());
}
}
return env;
}
然后NativeEnvironmentRepository
是放在environmentRepositories
里,所以其是在CompositeEnvironmentRepository
中再去调用NativeEnvironmentRepository
。
三、NativeEnvironmentRepository
1、基本结构
public class NativeEnvironmentRepository
implements EnvironmentRepository, SearchPathLocator, Ordered {
2、变量
private String defaultLabel;
/**
* Locations to search for configuration files. Defaults to the same as a Spring Boot
* app so [classpath:/,classpath:/config/,file:./,file:./config/].
*/
private String[] searchLocations;
/**
* Flag to determine how to handle exceptions during decryption (default false).
*/
private boolean failOnError;
/**
* Flag to determine whether label locations should be added.
*/
private boolean addLabelLocations;
/**
* Version string to be reported for native repository
*/
private String version;
private static final String[] DEFAULT_LOCATIONS = new String[] { "classpath:/",
"classpath:/config/", "file:./", "file:./config/" };
private ConfigurableEnvironment environment;
defaultLabel
:默认的label
searchLocations
:表示进行文件获取的时候的搜索路径
addLabelLocations
:表示lable
的值是否也参加进对应的路径
DEFAULT_LOCATIONS
:表示如果searchLocations
没有设置值的时候是默认在这些路径下去搜索对应的文件
3、构造方法
public NativeEnvironmentRepository(ConfigurableEnvironment environment, NativeEnvironmentProperties properties) {
this.environment = environment;
this.addLabelLocations = properties.getAddLabelLocations();
this.defaultLabel = properties.getDefaultLabel();
this.failOnError = properties.getFailOnError();
this.order = properties.getOrder();
this.searchLocations = properties.getSearchLocations();
this.version = properties.getVersion();
}
可以看到其主要是从NativeEnvironmentProperties
中读取对应的信息进行赋值
4、NativeEnvironmentProperties
1)、结构
@ConfigurationProperties("spring.cloud.config.server.native")
public class NativeEnvironmentProperties implements EnvironmentRepositoryProperties {
所以你可以通过再配置文件中设置这些来给NativeEnvironmentRepository的属性进行赋值。
2)、变量
private Boolean failOnError = false;
/**
* Flag to determine whether label locations should be added.
*/
private Boolean addLabelLocations = true;
private String defaultLabel = "master";
/**
* Locations to search for configuration files. Defaults to the same as a Spring Boot
* app so [classpath:/,classpath:/config/,file:./,file:./config/].
*/
private String[] searchLocations = new String[0];
/**
* Version string to be reported for native repository
*/
private String version;
private int order = Ordered.LOWEST_PRECEDENCE;
可以看到其的addLabelLocations
是默认为true,defaultLabel
默认为master
分支
5、findOne(…)方法
@Override
public Environment findOne(String config, String profile, String label) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(
PropertyPlaceholderAutoConfiguration.class);
ConfigurableEnvironment environment = getEnvironment(profile);
builder.environment(environment);
builder.web(WebApplicationType.NONE).bannerMode(Mode.OFF);
if (!logger.isDebugEnabled()) {
// Make the mini-application startup less verbose
builder.logStartupInfo(false);
}
String[] args = getArgs(config, profile, label);
// Explicitly set the listeners (to exclude logging listener which would change
// log levels in the caller)
builder.application()
.setListeners(Arrays.asList(new ConfigFileApplicationListener()));
ConfigurableApplicationContext context = builder.run(args);
environment.getPropertySources().remove("profiles");
try {
return clean(new PassthruEnvironmentRepository(environment).findOne(config,
profile, label));
}
finally {
context.close();
}
}
这个方法就是获取Environment的具体逻辑处理方法。这里首先是创建SpringApplicationBuilder
对象。再进行对应的环境赋值,之后再进行Web容器的运行builder.run(args)
,其是这里的运行主要是为了去触发ConfigFileApplicationListener
监听器的逻辑,在这个ConfigFileApplicationListener
中会完成对应文件的读取及构建对应的Envirement。下面我们就来看下这个具体的过程:
1)、getEnvironment(profile)
private ConfigurableEnvironment getEnvironment(String profile) {
ConfigurableEnvironment environment = new StandardEnvironment();
Map<String, Object> map = new HashMap<>();
map.put("spring.profiles.active", profile);
map.put("spring.main.web-application-type", "none");
environment.getPropertySources().addFirst(new MapPropertySource("profiles", map));
return environment;
}
创建一个基本的Envirement StandardEnvironment
对象,然后就是将要获取profile
(哪种环境)与key spring.profiles.active
构建一个map,然后再以此创建一个对应的MapPropertySource添加到Environment中。
2)、getArgs(String application, String profile, String label)
private String[] getArgs(String application, String profile, String label) {
List<String> list = new ArrayList<String>();
String config = application;
if (!config.startsWith("application")) {
config = "application," + config;
}
list.add("--spring.config.name=" + config);
list.add("--spring.cloud.bootstrap.enabled=false");
list.add("--encrypt.failOnError=" + this.failOnError);
list.add("--spring.config.location=" + StringUtils.arrayToCommaDelimitedString(
getLocations(application, profile, label).getLocations()));
return list.toArray(new String[0]);
}
这里首先我们需要注意,config = "application," + config
,也就是说,SpringCloudConfig不单是会去读取传入的应用名称对应的文件,也会默认读取application.yml
或application.properties
文件。
然后就是--spring.config.location=
(读取文件的时候是在那些路径下读取)的设置:
@Override
public Locations getLocations(String application, String profile, String label) {
String[] locations = this.searchLocations;
if (this.searchLocations == null || this.searchLocations.length == 0) {
locations = DEFAULT_LOCATIONS;
}
Collection<String> output = new LinkedHashSet<String>();
if (label == null) {
label = defaultLabel;
}
for (String location : locations) {
String[] profiles = new String[] { profile };
if (profile != null) {
profiles = StringUtils.commaDelimitedListToStringArray(profile);
}
String[] apps = new String[] { application };
if (application != null) {
apps = StringUtils.commaDelimitedListToStringArray(application);
}
for (String prof : profiles) {
for (String app : apps) {
String value = location;
if (application != null) {
value = value.replace("{application}", app);
}
if (prof != null) {
value = value.replace("{profile}", prof);
}
if (label != null) {
value = value.replace("{label}", label);
}
if (!value.endsWith("/")) {
value = value + "/";
}
if (isDirectory(value)) {
output.add(value);
}
}
}
}
if (this.addLabelLocations) {
for (String location : locations) {
if (StringUtils.hasText(label)) {
String labelled = location + label.trim() + "/";
if (isDirectory(labelled)) {
output.add(labelled);
}
}
}
}
return new Locations(application, profile, label, this.version,
output.toArray(new String[0]));
}
1、首先是通过String[] locations = this.searchLocations
,设置对应的目录地址,可以看到,如果locations
目前还没有设置值,就将其设置默认值DEFAULT_LOCATIONS
。
2、如果label
也没有设值也通过label = defaultLabel
设置默认值master
。
3、再是通过String[] profiles = new String[] { profile }
、String[] apps = new String[] { application }
对profile
、application
的解析。StringUtils.commaDelimitedListToStringArray(...)
方法是通过,
来对字符串解析切割。由这里我们看到profile
&application
都是可以配置读取多个对应组合的文件的,其是用,
来进行分隔。
4、之后再是对{application}
这种的替换。再通过isDirectory
方法来判断这个是不是地址:
private boolean isDirectory(String location) {
return !location.contains("{") && !location.endsWith(".properties")
&& !location.endsWith(".yml") && !location.endsWith(".yaml");
}
是的话就将其添加output
中。
5、再通过对addLabelLocations
的判断,看是否label
也参与目录组合(默认为true
),是的会就通过String labelled = location + label.trim() + "/";
来组合路径。
3)、findOne结束逻辑
对于整个getArgs
方法来说,就是将Locations的locations
返回。再通过arrayToCommaDelimitedString
方法来将其拼接为一个字符串(以,
划分)
public static String arrayToCommaDelimitedString(@Nullable Object[] arr) {
return arrayToDelimitedString(arr, ",");
}
最后对于findOne
方法来说:
return clean(new PassthruEnvironmentRepository(environment).findOne(config,
profile, label));
protected Environment clean(Environment value) {
Environment result = new Environment(value.getName(), value.getProfiles(),
value.getLabel(), this.version, value.getState());
for (PropertySource source : value.getPropertySources()) {
String name = source.getName();
if (this.environment.getPropertySources().contains(name)) {
continue;
}
......
logger.info("Adding property source: " + name);
result.add(new PropertySource(name, source.getSource()));
}
return result;
}
这里的逻辑,其就是将前面创建读取赋值的StandardEnvirement
中的PropertySources
添加到Environment
中来用于本次远程调用的返回(这个StandardEnvirement
的赋值我们后面就会分析到)。注意这个Envirement
与接口Envirement
的区别,这个是package org.springframework.cloud.config.environment;
包中的,这个Envirement
是一个类。
public class Environment {
private String name;
private String[] profiles = new String[0];
private String label;
private List<PropertySource> propertySources = new ArrayList<>();
private String version;
private String state;
下面我们来梳理下前面提到的ConfigFileApplicationListener
对配置文件的读取解析主要就是这个类。
四、ConfigFileApplicationListener
1、结构&变量
public class ConfigFileApplicationListener
implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
private static final String DEFAULT_PROPERTIES = "defaultProperties";
// Note the order is from least to most specific (last one wins)
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);
/**
* The "active profiles" property name.
*/
public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
/**
* The "includes profiles" property name.
*/
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
/**
* The "config name" property name.
*/
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
/**
* The "config location" property name.
*/
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
/**
* The "config additional location" property name.
*/
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
/**
* The default order for the processor.
*/
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
/**
* Name of the application configuration {@link PropertySource}.
*/
public static final String APPLICATION_CONFIGURATION_PROPERTY_SOURCE_NAME = "applicationConfigurationProperties";
private final DeferredLog logger = new DeferredLog();
private String searchLocations;
private String names;
private int order = DEFAULT_ORDER;
这里的变量主要是定义一些默认值,对于这些值通过前面的梳理,我们也是眼熟了。
2、postProcessEnvironment方法
@Override
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();
}
这就是触发加载配置文件并将其添加到environment
中的MutablePropertySources
中的逻辑。主要处理的类就是Loader
。下面我们具体来分析下这个类。
五、Loader
1、结构&变量
private class Loader {
private final Log logger = ConfigFileApplicationListener.this.logger;
private final ConfigurableEnvironment environment;
private final ResourceLoader resourceLoader;
private final List<PropertySourceLoader> propertySourceLoaders;
private Deque<Profile> profiles;
private List<Profile> processedProfiles;
private boolean activatedProfiles;
private Map<Profile, MutablePropertySources> loaded;
private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.resourceLoader = (resourceLoader != null ? resourceLoader
: new DefaultResourceLoader());
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
PropertySourceLoader.class, getClass().getClassLoader());
}
其是ConfigFileApplicationListener的内部类。
2、load方法
public void load() {
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 (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
1)、这里先是通过initializeProfiles
方法来对profiles
赋值,然后对其进行遍历读取。
2)、先通过addProfileToEnvironment
,来将该profile
设置为激活的profile
。
3)、再通过load(...)
方法来进行解析读取配置文件。
4)、最后通过addLoadedPropertySources
方法来将读取的PropertySource
添加到envirement
中。
5)、这些就是其的主要处理逻辑,为了更好的分层排版,我们将这个方法调用的其它方法也写在第二级。
3、initializeProfiles()
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);
Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
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);
}
}
}
这个方法是对profiles
的赋值,可以看到这里首先是this.profiles.add(null)
,这个赋值的意义类似与前面默认添加的application
。例如你的应用名是configClient
(application
),然后你要读取的环境是dev
(profile
),所以拼接就可以是文件configClient_dev.yml
,然后由于有这个赋值,其也会读取configClient.yml
,不管你传入的profile
是哪个,会以{application}.yml
为公共的配置文件。同时如果this.profiles.size() == 1
,表示这里只有默认的null,其也会添加一个默认的profile名称default
,也就是RESERVED_DEFAULT_PROFILE_NAME = "default";
。不过由于我们传入了dev
,所以这里是有两个:
1)、getProfilesActivatedViaProperty()
从envirement
中读取这些key 所对应的value。
4、addProfileToEnvironment(String profile)
private void addProfileToEnvironment(String profile) {
for (String activeProfile : this.environment.getActiveProfiles()) {
if (activeProfile.equals(profile)) {
return;
}
}
this.environment.addActiveProfile(profile);
}
@Override
public void addActiveProfile(String profile) {
if (logger.isDebugEnabled()) {
logger.debug("Activating profile '" + profile + "'");
}
validateProfile(profile);
doGetActiveProfiles();
synchronized (this.activeProfiles) {
this.activeProfiles.add(profile);
}
}
5、load(Profile profile, …)
private void load(Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
}
这里就是遍历搜索目录。
1)、getSearchLocations()
private Set<String> getSearchLocations() {
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
Set<String> locations = getSearchLocations(
CONFIG_ADDITIONAL_LOCATION_PROPERTY);
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
return locations;
}
如果有设置对应的读取路径,就直接返回设置的值。如果没有设置就返回默认的读取路径。
2)、getSearchNames()
private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
return asResolvedSet(property, null);
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}
读取应用名称,我们这里的就是application,config-client
。
3)、load(location, name, profile, filterFactory, consumer)
private void load(String location, String name, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile,
filterFactory.getDocumentFilter(profile), consumer);
}
}
}
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
String prefix = location + name;
fileExtension = "." + fileExtension;
loadForFileExtension(loader, prefix, fileExtension, profile,
filterFactory, consumer);
}
}
}
这里的this.propertySourceLoaders
(PropertySourceLoader
)是用来加载对应文件的。这里其会有两个子类:
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
private static final String XML_FILE_EXTENSION = ".xml";
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
可以看到这里的YamlPropertySourceLoader是用来读取以.yml
、yaml
为后缀的文件,PropertiesPropertySourceLoader是用来读取.properties
、xml
文件的。然后上面的load(loader, location, profile...)
方法我们到后面再梳理,因为其会在下面的loadForFileExtension(...)
方法中调用**(注意String prefix = location + name;
)**。我们主要来看下一个对this.propertySourceLoaders
的for循环。上面的是如果location
本身就是一个文件具体的地址了,就调用load方法。
6、loadForFileExtension
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)
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);
}
这里我们需要注意对文件名称的整个拼接,profile != null
就是表示如果profile是有值的,其的拼接是String profileSpecificFile = prefix + "-" + profile + fileExtension
,如果其为null
就是通过最后的load方法,其的加载名称是prefix + fileExtension
。下面我们就来具体看下load
方法。
7、load(PropertySourceLoader loader, String location,…)
private void load(PropertySourceLoader loader, String location, Profile profile,
DocumentFilter filter, DocumentConsumer consumer) {
try {
Resource resource = this.resourceLoader.getResource(location);
String description = getDescription(location, resource);
......
String name = "applicationConfig: [" + location + "]";
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
this.logger.trace("Skipped unloaded config " + description);
return;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
this.logger.debug("Loaded config file " + description);
}
}
......
}
1)、这里首先是通过Resource resource = this.resourceLoader.getResource(location)
来加载对应的文件。
2)、再通过List<Document> documents = loadDocuments(loader, name, resource);
来将对应的文件内容加载为Document
private static class Document {
private final PropertySource<?> propertySource;
private String[] profiles;
private final Set<Profile> activeProfiles;
private final Set<Profile> includeProfiles;
3)、然后再遍历将其添加到loaded.add(document);
中。
4)、再就是通过loaded.forEach((document) -> consumer.accept(profile, document));
consumer
去处理了。这里是入参,我们一直回到最前面,其的传值是在一开始了
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
......
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
......
addLoadedPropertySources();
}
private DocumentFilter getPositiveProfileFilter(Profile profile) {
return (Document document) -> {
if (profile == null) {
return ObjectUtils.isEmpty(document.getProfiles());
}
return ObjectUtils.containsElement(document.getProfiles(),
profile.getName())
&& this.environment.acceptsProfiles(document.getProfiles());
};
}
private DocumentConsumer addToLoaded(
BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}
可以看到这里的DocumentConsumer
其实是调用MutablePropertySources::addLast
、this.loaded.computeIfAbsent(...)
方法链。也就是读取Document
的内容document.getPropertySource()
,再调用的addLast
方法来将其添加到this.loaded
中(private Map<Profile, MutablePropertySources> loaded;
)。
8、addLoadedPropertySources()
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
这个方法就是将前面读取到this.loaded
中的内容通过addLoadedPropertySource
方法将其添加到this.environment
中。这里就填你上面说的对StandardEnvirement
赋值的问题了。
至此,关于SpringCloudConfig读取远程配置文件的原理就梳理完成了。