Spring 资源加载小解

Posted by CaiJiahe on October 27, 2017

0x01 PropertySourceLocator

PropertySourceLocator可以实现从外部配置加载到bootstrap context中。

	# Bootstrap components
	org.springframework.cloud.bootstrap.BootstrapConfiguration=\
	org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,\
	org.springframework.cloud.bootstrap.encrypt.EncryptionBootstrapConfiguration,\
	org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration,\
	org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration

从factories可以看到PropertySourceBootstrapConfiguration的存在。

// spring-cloud-context o.s.c.bootstrap.config
@Configuration
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
		ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

	@Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

	@Override
	public int getOrder() {
		return this.order;
	}

	@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;
			}
			logger.info("Located property source: " + source);
			composite.addPropertySource(source);
			empty = false;
		}
		if (!empty) {
			MutablePropertySources propertySources = environment.getPropertySources();
			String logConfig = environment.resolvePlaceholders("${logging.config:}");
			LogFile logFile = LogFile.get(environment);
			if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
				propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
			}
			insertPropertySources(propertySources, composite);
			reinitializeLoggingSystem(environment, logConfig, logFile);
			setLogLevels(environment);
			handleIncludedProfiles(environment);
		}
	}

	private void insertPropertySources(MutablePropertySources propertySources,
			CompositePropertySource composite) {
		MutablePropertySources incoming = new MutablePropertySources();
		incoming.addFirst(composite);
		PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
		new RelaxedDataBinder(remoteProperties, "spring.cloud.config")
				.bind(new PropertySourcesPropertyValues(incoming));
		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);
		}
	}
	
	private void handleIncludedProfiles(ConfigurableEnvironment environment) {
		Set<String> includeProfiles = new TreeSet<>();
		for (PropertySource<?> propertySource : environment.getPropertySources()) {
			addIncludedProfilesTo(includeProfiles, propertySource);
		}
		List<String> activeProfiles = new ArrayList<>();
		Collections.addAll(activeProfiles, environment.getActiveProfiles());

		// If it's already accepted we assume the order was set intentionally
		includeProfiles.removeAll(activeProfiles);
		if (includeProfiles.isEmpty()) {
			return;
		}
		// Prepend each added profile (last wins in a property key clash)
		for (String profile : includeProfiles) {
			activeProfiles.add(0, profile);
		}
		environment.setActiveProfiles(
				activeProfiles.toArray(new String[activeProfiles.size()]));
	}

	...

}

首先对locator进行排序,然后遍历开始执行locate方法,将返回的PropertySource加入到CompositePropertySource中。如果locate的PropertySource不为空,则对其进行一些过滤。
可以看到PropertySourceLocator的子类有ConfigServicePropertySourceLocator和EnvironmentRepositoryPropertySourceLocator。一般我们使用ConfigServicePropertySourceLocator来加载配置中心的配置。
默认的,远程配置是不能够被本地配置覆盖的,但是可以在config server设置spring.cloud.config.allowOverride=true。设置好这个参数,可以使用spring.cloud.config.overrideNone和spring.cloud.config.overrideSystemProperties来控制本地配置可以覆盖远程配置的范围。代码中就是通过BOOTSTRAP_PROPERTY_SOURCE_NAME放在propertySources中的顺序来保证其是否可以覆盖远程配置。

0x02 PropertySourceLoader

资源文件加载的地方。

	# PropertySource Loaders
	org.springframework.boot.env.PropertySourceLoader=\
	org.springframework.boot.env.PropertiesPropertySourceLoader,\
	org.springframework.boot.env.YamlPropertySourceLoader
	
	# Application Listeners
	org.springframework.context.ApplicationListener=\
	org.springframework.boot.ClearCachesApplicationListener,\
	org.springframework.boot.builder.ParentContextCloserApplicationListener,\
	org.springframework.boot.context.FileEncodingApplicationListener,\
	org.springframework.boot.context.config.AnsiOutputApplicationListener,\
	org.springframework.boot.context.config.ConfigFileApplicationListener,\
	org.springframework.boot.context.config.DelegatingApplicationListener,\
	org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
	org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
	org.springframework.boot.logging.LoggingApplicationListener
	
	# Environment Post Processors
	org.springframework.boot.env.EnvironmentPostProcessor=\
	org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
	org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor

ApplicationListener里包含ConfigFileApplicationListener这个ApplicationListener,这个负责开始加载配置。
可以看到注册了两个PropertySourceLoader,PropertiesPropertySourceLoader和YamlPropertySourceLoader。

// spring-boot o.s.b.context.config
public class ConfigFileApplicationListener
		implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent(
					(ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
	
	private void onApplicationEnvironmentPreparedEvent(
			ApplicationEnvironmentPreparedEvent event) {
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(),
					event.getSpringApplication());
		}
	}
	
	List<EnvironmentPostProcessor> loadPostProcessors() {
		return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class,
				getClass().getClassLoader());
	}
	
	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
		configureIgnoreBeanInfo(environment);
		bindToSpringApplication(environment, application);
	}
	
	protected void addPropertySources(ConfigurableEnvironment environment,
			ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		new Loader(environment, resourceLoader).load();
	}
		
}

这里可以看到ConfigFileApplicationListener是ApplicationListener和EnvironmentPostProcessor的子类。在启动的时候如果收到了ApplicationEnvironmentPreparedEvent事件,则开始去加载所有注册的EnvironmentPostProcessor去执行。注意,在加载完factories里注册的EnvironmentPostProcessor后,ConfigFileApplicationListener把自己也加入postProcessors中,然后去执行。
下面再看下本类的postProcessEnvironment方法,首先就是调用了addPropertySources,使用Loader去加载配置。

	/**
	 * Loads candidate property sources and configures the active profiles.
	 */
	private class Loader {

		private final Log logger = ConfigFileApplicationListener.this.logger;

		private final ConfigurableEnvironment environment;

		private final ResourceLoader resourceLoader;

		private PropertySourcesLoader propertiesLoader;

		private Queue<Profile> profiles;

		private List<Profile> processedProfiles;

		private boolean activatedProfiles;

		Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
			this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
					: resourceLoader;
		}

		public void load() {
			this.propertiesLoader = new PropertySourcesLoader();
			this.activatedProfiles = false;
			this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
			this.processedProfiles = new LinkedList<Profile>();

			// Pre-existing active profiles set via Environment.setActiveProfiles()
			// are additional profiles and config files are allowed to add more if
			// they want to, so don't call addActiveProfiles() here.
			Set<Profile> initialActiveProfiles = initializeActiveProfiles();
			this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
			if (this.profiles.isEmpty()) {
				for (String defaultProfileName : this.environment.getDefaultProfiles()) {
					Profile defaultProfile = new Profile(defaultProfileName, true);
					if (!this.profiles.contains(defaultProfile)) {
						this.profiles.add(defaultProfile);
					}
				}
			}

			// The default profile for these purposes is represented as null. We add it
			// last so that it is first out of the queue (active profiles will then
			// override any settings in the defaults when the list is reversed later).
			this.profiles.add(null);

			while (!this.profiles.isEmpty()) {
				Profile profile = this.profiles.poll();
				for (String location : getSearchLocations()) {
					if (!location.endsWith("/")) {
						// location is a filename already, so don't search for more
						// filenames
						load(location, null, profile);
					}
					else {
						for (String name : getSearchNames()) {
							load(location, name, profile);
						}
					}
				}
				this.processedProfiles.add(profile);
			}

			addConfigurationProperties(this.propertiesLoader.getPropertySources());
		}

		private Set<Profile> initializeActiveProfiles() {
			if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
					&& !this.environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
				return Collections.emptySet();
			}
			// Any pre-existing active profiles set via property sources (e.g. System
			// properties) take precedence over those added in config files.
			SpringProfiles springProfiles = bindSpringProfiles(
					this.environment.getPropertySources());
			Set<Profile> activeProfiles = new LinkedHashSet<Profile>(
					springProfiles.getActiveProfiles());
			activeProfiles.addAll(springProfiles.getIncludeProfiles());
			maybeActivateProfiles(activeProfiles);
			return activeProfiles;
		}

		/**
		 * Return the active profiles that have not been processed yet. If a profile is
		 * enabled via both {@link #ACTIVE_PROFILES_PROPERTY} and
		 * {@link ConfigurableEnvironment#addActiveProfile(String)} it needs to be
		 * filtered so that the {@link #ACTIVE_PROFILES_PROPERTY} value takes precedence.
		 * <p>
		 * Concretely, if the "cloud" profile is enabled via the environment, it will take
		 * less precedence that any profile set via the {@link #ACTIVE_PROFILES_PROPERTY}.
		 * @param initialActiveProfiles the profiles that have been enabled via
		 * {@link #ACTIVE_PROFILES_PROPERTY}
		 * @return the unprocessed active profiles from the environment to enable
		 */
		private List<Profile> getUnprocessedActiveProfiles(
				Set<Profile> initialActiveProfiles) {
			List<Profile> unprocessedActiveProfiles = new ArrayList<Profile>();
			for (String profileName : this.environment.getActiveProfiles()) {
				Profile profile = new Profile(profileName);
				if (!initialActiveProfiles.contains(profile)) {
					unprocessedActiveProfiles.add(profile);
				}
			}
			// Reverse them so the order is the same as from getProfilesForValue()
			// (last one wins when properties are eventually resolved)
			Collections.reverse(unprocessedActiveProfiles);
			return unprocessedActiveProfiles;
		}

		private void load(String location, String name, Profile profile) {
			String group = "profile=" + (profile == null ? "" : profile);
			if (!StringUtils.hasText(name)) {
				// Try to load directly from the location
				loadIntoGroup(group, location, profile);
			}
			else {
				// Search for a file with the given name
				for (String ext : this.propertiesLoader.getAllFileExtensions()) {
					if (profile != null) {
						// Try the profile-specific file
						loadIntoGroup(group, location + name + "-" + profile + "." + ext,
								null);
						for (Profile processedProfile : this.processedProfiles) {
							if (processedProfile != null) {
								loadIntoGroup(group, location + name + "-"
										+ processedProfile + "." + ext, profile);
							}
						}
						// Sometimes people put "spring.profiles: dev" in
						// application-dev.yml (gh-340). Arguably we should try and error
						// out on that, but we can be kind and load it anyway.
						loadIntoGroup(group, location + name + "-" + profile + "." + ext,
								profile);
					}
					// Also try the profile-specific section (if any) of the normal file
					loadIntoGroup(group, location + name + "." + ext, profile);
				}
			}
		}

		private PropertySource<?> loadIntoGroup(String identifier, String location,
				Profile profile) {
			try {
				return doLoadIntoGroup(identifier, location, profile);
			}
			catch (Exception ex) {
				throw new IllegalStateException(
						"Failed to load property source from location '" + location + "'",
						ex);
			}
		}

		private PropertySource<?> doLoadIntoGroup(String identifier, String location,
				Profile profile) throws IOException {
			Resource resource = this.resourceLoader.getResource(location);
			PropertySource<?> propertySource = null;
			StringBuilder msg = new StringBuilder();
			if (resource != null && resource.exists()) {
				String name = "applicationConfig: [" + location + "]";
				String group = "applicationConfig: [" + identifier + "]";
				propertySource = this.propertiesLoader.load(resource, group, name,
						(profile == null ? null : profile.getName()));
				if (propertySource != null) {
					msg.append("Loaded ");
					handleProfileProperties(propertySource);
				}
				else {
					msg.append("Skipped (empty) ");
				}
			}
			else {
				msg.append("Skipped ");
			}
			msg.append("config file ");
			msg.append(getResourceDescription(location, resource));
			if (profile != null) {
				msg.append(" for profile ").append(profile);
			}
			if (resource == null || !resource.exists()) {
				msg.append(" resource not found");
				this.logger.trace(msg);
			}
			else {
				this.logger.debug(msg);
			}
			return propertySource;
		}

		private String getResourceDescription(String location, Resource resource) {
			String resourceDescription = "'" + location + "'";
			if (resource != null) {
				try {
					resourceDescription = String.format("'%s' (%s)",
							resource.getURI().toASCIIString(), location);
				}
				catch (IOException ex) {
					// Use the location as the description
				}
			}
			return resourceDescription;
		}

		private void handleProfileProperties(PropertySource<?> propertySource) {
			SpringProfiles springProfiles = bindSpringProfiles(propertySource);
			maybeActivateProfiles(springProfiles.getActiveProfiles());
			addProfiles(springProfiles.getIncludeProfiles());
		}

		private SpringProfiles bindSpringProfiles(PropertySource<?> propertySource) {
			MutablePropertySources propertySources = new MutablePropertySources();
			propertySources.addFirst(propertySource);
			return bindSpringProfiles(propertySources);
		}

		private SpringProfiles bindSpringProfiles(PropertySources propertySources) {
			SpringProfiles springProfiles = new SpringProfiles();
			RelaxedDataBinder dataBinder = new RelaxedDataBinder(springProfiles,
					"spring.profiles");
			dataBinder.bind(new PropertySourcesPropertyValues(propertySources, false));
			springProfiles.setActive(resolvePlaceholders(springProfiles.getActive()));
			springProfiles.setInclude(resolvePlaceholders(springProfiles.getInclude()));
			return springProfiles;
		}

		private List<String> resolvePlaceholders(List<String> values) {
			List<String> resolved = new ArrayList<String>();
			for (String value : values) {
				resolved.add(this.environment.resolvePlaceholders(value));
			}
			return resolved;
		}

		private void maybeActivateProfiles(Set<Profile> profiles) {
			if (this.activatedProfiles) {
				if (!profiles.isEmpty()) {
					this.logger.debug("Profiles already activated, '" + profiles
							+ "' will not be applied");
				}
				return;
			}
			if (!profiles.isEmpty()) {
				addProfiles(profiles);
				this.logger.debug("Activated profiles "
						+ StringUtils.collectionToCommaDelimitedString(profiles));
				this.activatedProfiles = true;
				removeUnprocessedDefaultProfiles();
			}
		}

		private void removeUnprocessedDefaultProfiles() {
			for (Iterator<Profile> iterator = this.profiles.iterator(); iterator
					.hasNext();) {
				if (iterator.next().isDefaultProfile()) {
					iterator.remove();
				}
			}
		}

		private void addProfiles(Set<Profile> profiles) {
			for (Profile profile : profiles) {
				this.profiles.add(profile);
				if (!environmentHasActiveProfile(profile.getName())) {
					// If it's already accepted we assume the order was set
					// intentionally
					prependProfile(this.environment, profile);
				}
			}
		}

		private boolean environmentHasActiveProfile(String profile) {
			for (String activeProfile : this.environment.getActiveProfiles()) {
				if (activeProfile.equals(profile)) {
					return true;
				}
			}
			return false;
		}

		private void prependProfile(ConfigurableEnvironment environment,
				Profile profile) {
			Set<String> profiles = new LinkedHashSet<String>();
			environment.getActiveProfiles(); // ensure they are initialized
			// But this one should go first (last wins in a property key clash)
			profiles.add(profile.getName());
			profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
			environment.setActiveProfiles(profiles.toArray(new String[profiles.size()]));
		}

		private Set<String> getSearchLocations() {
			Set<String> locations = new LinkedHashSet<String>();
			// User-configured settings take precedence, so we do them first
			if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
				for (String path : asResolvedSet(
						this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
					if (!path.contains("$")) {
						path = StringUtils.cleanPath(path);
						if (!ResourceUtils.isUrl(path)) {
							path = ResourceUtils.FILE_URL_PREFIX + path;
						}
					}
					locations.add(path);
				}
			}
			locations.addAll(
					asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
							DEFAULT_SEARCH_LOCATIONS));
			return locations;
		}

		private Set<String> getSearchNames() {
			if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
				return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
						null);
			}
			return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
		}

		private Set<String> asResolvedSet(String value, String fallback) {
			List<String> list = Arrays.asList(StringUtils.trimArrayElements(
					StringUtils.commaDelimitedListToStringArray(value != null
							? this.environment.resolvePlaceholders(value) : fallback)));
			Collections.reverse(list);
			return new LinkedHashSet<String>(list);
		}

		private void addConfigurationProperties(MutablePropertySources sources) {
			List<PropertySource<?>> reorderedSources = new ArrayList<PropertySource<?>>();
			for (PropertySource<?> item : sources) {
				reorderedSources.add(item);
			}
			addConfigurationProperties(
					new ConfigurationPropertySources(reorderedSources));
		}

		private void addConfigurationProperties(
				ConfigurationPropertySources configurationSources) {
			MutablePropertySources existingSources = this.environment
					.getPropertySources();
			if (existingSources.contains(DEFAULT_PROPERTIES)) {
				existingSources.addBefore(DEFAULT_PROPERTIES, configurationSources);
			}
			else {
				existingSources.addLast(configurationSources);
			}
		}

	}

可以看到load方法中,首先根据profiles去加载相应的配置。如果没有profile配置则直接去加载默认的配置,否则会根据profile、后缀名去拼凑文件名去加载。