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、后缀名去拼凑文件名去加载。