我们知道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接口。下面我们来看其的方法调用:
@Overridepublic 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)、(_)替换符处理
可以看到这里有一种操作将(_)转换为/,这里其实就一种转换,因为如果将/放在请求上是代表的另一个请求,所以这里是有一个替换,这个替换时请求获取的时候可内容请求本身还有一层路径:
@Testpublic 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 {复制代码
@Overridepublic 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;复制代码
@Overridepublic 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。
三、NativeEnvironmentRepository1、基本结构
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(......)方法
@Overridepublic 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=(读取文件的时候是在那些路径下读取)的设置:
@Overridepublic 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对配置文件的读取解析主要就是这个类。
四、ConfigFileApplicationListener1、结构&变量
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方法
@Overridepublic 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。下面我们具体来分析下这个类。
五、Loader1、结构&变量
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); }复制代码
@Overridepublic 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读取远程配置文件的原理就梳理完成了。