Add Environment to ProeprtySourceLocator.locate()

In this way each contexet in a hierarchy can in principle have a different
set of property sources
This commit is contained in:
Dave Syer
2014-10-30 11:43:00 +00:00
parent 010d29c124
commit 9efae007fa
11 changed files with 381 additions and 186 deletions

View File

@@ -352,12 +352,15 @@ interface, or with the `SpringApplicationBuilder` convenience methods
(`parent()`, `child()` and `sibling()`). Note that the
`SpringApplicationBuilder` allows you to share an `Environment`
amongst the whole hierarchy, but that is not the default. Thus,
normally you expect to see differences between different levels in the
hierarchy, and sibling contexts in particular do not need to have the
same profiles or property sources, even though they will share common
things with their parent. Every context in the hierarchy will have its
own "bootstrap" property source (possibly empty) to avoid promoting
values inadvertently from parents down to their descendants.
sibling contexts in particular do not need to have the same profiles
or property sources, even though they will share common things with
their parent. Every context in the hierarchy will have its own
"bootstrap" property source (possibly empty) to avoid promoting values
inadvertently from parents down to their descendants. Every context in
the hierarchy can also (in principle) have a different
`spring.application.name` and hence a different remote property source
if there is a Config Server. Remember that properties from a child
context override those in the parent.
[[customizing-bootstrap-properties]]
=== Changing the Location of Bootstrap Properties
@@ -411,13 +414,22 @@ As an example, consider the following trivial custom locator:
public class CustomPropertySourceLocator implements PropertySourceLocator {
@Override
public PropertySource<?> locate() {
public PropertySource<?> locate(Environment environment) {
return new MapPropertySource("customProperty",
Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended"));
}
}
----
The `Environment` that is passed in is the one for the
`ApplicationContext` about to be created, i.e. the one that we are
supplying additional property sources for. It will already have its
normal Spring Boot-provided property sources, so you can use those to
locate a property source specific to this `Environment` (e.g. by
keying it on the `spring.application.name`, as is done in the default
Config Server property source locator).
If you create a jar with this class in it and then add a
`META-INF/spring.factories` containing:

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.autoconfigure;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* Expose a ConfigClientProperties just so that there is a way to inspect the properties
* bound to it. It won't be available in time for autowiring into the bootstrap context,
* but the values in this properties object will be the same as the ones used to bind to
* the config server, if there is one.
*
* @author Dave Syer
*
*/
@Configuration
public class ConfigClientAutoConfiguration {
@Bean
public ConfigClientProperties configClientProperties(Environment environment) {
ConfigClientProperties client = new ConfigClientProperties(environment);
return client;
}
}

View File

@@ -24,6 +24,10 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bootstrap.BootstrapApplicationListener;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
import org.springframework.cloud.config.client.PropertySourceLocator;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -33,10 +37,6 @@ import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.cloud.bootstrap.BootstrapApplicationListener;
import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator;
import org.springframework.cloud.config.client.PropertySourceLocator;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
@@ -70,7 +70,7 @@ public class PropertySourceBootstrapConfiguration implements
for (PropertySourceLocator locator : propertySourceLocators) {
PropertySource<?> source = null;
try {
source = locator.locate();
source = locator.locate(applicationContext.getEnvironment());
}
catch (Exception e) {
logger.error("Could not locate PropertySource: " + e.getMessage());
@@ -94,16 +94,25 @@ public class PropertySourceBootstrapConfiguration implements
}
}
@Bean
public ConfigServicePropertySourceLocator configServicePropertySource(
ConfigurableEnvironment environment) {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator();
String[] profiles = environment.getActiveProfiles();
if (profiles.length == 0) {
profiles = environment.getDefaultProfiles();
@Configuration
protected static class PropertySourceLocatorConfiguration {
@Autowired
private ConfigurableEnvironment environment;
@Bean
public ConfigClientProperties configClientProperties() {
ConfigClientProperties client = new ConfigClientProperties(environment);
return client;
}
locator.setEnv(StringUtils.arrayToCommaDelimitedString(profiles));
return locator;
@Bean
public ConfigServicePropertySourceLocator configServicePropertySource() {
ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
configClientProperties());
return locator;
}
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright 2013-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.config.client;
import java.net.MalformedURLException;
import java.net.URL;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Dave Syer
*
*/
@ConfigurationProperties(ConfigClientProperties.PREFIX)
public class ConfigClientProperties {
public static final String PREFIX = "spring.cloud.config";
private String env = "default";
@Value("${spring.application.name:'application'}")
private String name;
private String label = "master";
private String username;
private String password;
private String uri = "http://localhost:8888";
private Discovery discovery = new Discovery();
private ConfigClientProperties() {
}
public ConfigClientProperties(Environment environment) {
String[] profiles = environment.getActiveProfiles();
if (profiles.length == 0) {
profiles = environment.getDefaultProfiles();
}
this.setEnv(StringUtils.arrayToCommaDelimitedString(profiles));
}
public String getUri() {
return extractCredentials()[2];
}
public void setUri(String url) {
this.uri = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEnv() {
return env;
}
public void setEnv(String env) {
this.env = env;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getUsername() {
return extractCredentials()[0];
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return extractCredentials()[1];
}
public void setPassword(String password) {
this.password = password;
}
public Discovery getDiscovery() {
return discovery;
}
public void setDiscovery(Discovery discovery) {
this.discovery = discovery;
}
private String[] extractCredentials() {
String[] result = new String[3];
String uri = this.uri;
result[2] = uri;
String[] creds = getUsernamePassword();
result[0] = creds[0];
result[1] = creds[1];
try {
URL url = new URL(uri);
String userInfo = url.getUserInfo();
if (StringUtils.isEmpty(userInfo) || ":".equals(userInfo)) {
return result;
}
String bare = UriComponentsBuilder.fromHttpUrl(uri).userInfo(null).build()
.toUriString();
result[2] = bare;
if (!userInfo.contains(":")) {
userInfo = userInfo + ":";
}
String[] split = userInfo.split(":");
result[0] = split[0];
result[1] = split[1];
if (creds[1] != null) {
// Explicit username / password takes precedence
result[1] = creds[1];
if ("user".equals(creds[0])) {
// But the username can be overridden
result[0] = split[0];
}
}
return result;
}
catch (MalformedURLException e) {
throw new IllegalStateException("Invalid URL: " + uri);
}
}
private String[] getUsernamePassword() {
if (StringUtils.hasText(password)) {
return new String[] {
StringUtils.hasText(username) ? username.trim() : "user",
password.trim() };
}
return new String[2];
}
public static class Discovery {
public static final String DEFAULT_CONFIG_SERVER = "CONFIGSERVER";
private boolean enabled;
private String serviceId = DEFAULT_CONFIG_SERVER;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
}
public ConfigClientProperties override(
org.springframework.core.env.Environment environment) {
ConfigClientProperties override = new ConfigClientProperties();
BeanUtils.copyProperties(this, override);
override.setName(environment.resolvePlaceholders("${"
+ ConfigClientProperties.PREFIX
+ ".name:${spring.application.name:'application'}}"));
if (environment.containsProperty(ConfigClientProperties.PREFIX + ".env")) {
override.setEnv(environment.getProperty(ConfigClientProperties.PREFIX + ".env"));
}
if (environment.containsProperty(ConfigClientProperties.PREFIX + ".label")) {
override.setLabel(environment.getProperty(ConfigClientProperties.PREFIX + ".label"));
}
return override;
}
@Override
public String toString() {
return "ConfigClientProperties [name=" + name + ", env=" + env + ", label="
+ label + ", uri=" + uri + ", discovery.enabled=" + discovery.enabled + "]";
}
}

View File

@@ -17,13 +17,9 @@
package org.springframework.cloud.config.client;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.Environment;
import org.springframework.cloud.config.PropertySource;
import org.springframework.core.annotation.Order;
@@ -36,43 +32,33 @@ import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Dave Syer
*
*/
@ConfigurationProperties("spring.cloud.config")
@Order(0)
public class ConfigServicePropertySourceLocator implements PropertySourceLocator {
private String env = "default";
@Value("${spring.application.name:'application'}")
private String name;
private String label = "master";
private String username;
private String password;
private String uri = "http://localhost:8888";
private Discovery discovery = new Discovery();
private RestTemplate restTemplate;
private ConfigClientProperties defaults;
public ConfigServicePropertySourceLocator(ConfigClientProperties defaults) {
this.defaults = defaults;
}
@Override
public org.springframework.core.env.PropertySource<?> locate() {
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties client = defaults.override(environment);
CompositePropertySource composite = new CompositePropertySource("configService");
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate()
RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(client)
: this.restTemplate;
Environment result = restTemplate.exchange(getUri() + "/{name}/{env}/{label}",
HttpMethod.GET, new HttpEntity<Void>((Void) null), Environment.class,
name, env, label).getBody();
Environment result = restTemplate.exchange(
client.getUri() + "/{name}/{env}/{label}", HttpMethod.GET,
new HttpEntity<Void>((Void) null), Environment.class, client.getName(),
client.getEnv(), client.getLabel()).getBody();
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) source.getSource();
@@ -85,115 +71,17 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
this.restTemplate = restTemplate;
}
public String getUri() {
return extractCredentials()[2];
}
public void setUri(String url) {
this.uri = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEnv() {
return env;
}
public void setEnv(String env) {
this.env = env;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getUsername() {
return extractCredentials()[0];
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return extractCredentials()[1];
}
public void setPassword(String password) {
this.password = password;
}
public Discovery getDiscovery() {
return discovery;
}
private RestTemplate getSecureRestTemplate() {
private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
RestTemplate template = new RestTemplate();
String[] userInfo = extractCredentials();
if (userInfo[1] != null) {
String password = client.getPassword();
if (password != null) {
template.setInterceptors(Arrays
.<ClientHttpRequestInterceptor> asList(new BasicAuthorizationInterceptor(
userInfo[0], userInfo[1])));
client.getUsername(), password)));
}
return template;
}
private String[] extractCredentials() {
String[] result = new String[3];
String uri = this.uri;
result[2] = uri;
String[] creds = getUsernamePassword();
result[0] = creds[0];
result[1] = creds[1];
try {
URL url = new URL(uri);
String userInfo = url.getUserInfo();
if (StringUtils.isEmpty(userInfo) || ":".equals(userInfo)) {
return result;
}
String bare = UriComponentsBuilder.fromHttpUrl(uri).userInfo(null).build()
.toUriString();
result[2] = bare;
if (!userInfo.contains(":")) {
userInfo = userInfo + ":";
}
String[] split = userInfo.split(":");
result[0] = split[0];
result[1] = split[1];
if (creds[1] != null) {
// Explicit username / password takes precedence
result[1] = creds[1];
if ("user".equals(creds[0])) {
// But the username can be overridden
result[0] = split[0];
}
}
return result;
}
catch (MalformedURLException e) {
throw new IllegalStateException("Invalid URL: " + uri);
}
}
private String[] getUsernamePassword() {
if (StringUtils.hasText(password)) {
return new String[] {
StringUtils.hasText(username) ? username.trim() : "user",
password.trim() };
}
return new String[2];
}
private static class BasicAuthorizationInterceptor implements
ClientHttpRequestInterceptor {
@@ -217,27 +105,4 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator
}
public static class Discovery {
public static final String DEFAULT_CONFIG_SERVER = "CONFIGSERVER";
private boolean enabled;
private String serviceId = DEFAULT_CONFIG_SERVER;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.cloud.config.client;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
@@ -24,6 +25,6 @@ import org.springframework.core.env.PropertySource;
*/
public interface PropertySourceLocator {
PropertySource<?> locate();
PropertySource<?> locate(Environment environment);
}

View File

@@ -1,6 +1,7 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.autoconfigure.RefreshAutoConfiguration
org.springframework.cloud.autoconfigure.RefreshAutoConfiguration,\
org.springframework.cloud.autoconfigure.ConfigClientAutoConfiguration
# Application Listeners
org.springframework.context.ApplicationListener=\

View File

@@ -27,14 +27,17 @@ import java.util.Collections;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.client.PropertySourceLocator;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.cloud.config.client.PropertySourceLocator;
/**
* @author Dave Syer
@@ -46,6 +49,7 @@ public class BootstrapConfigurationTests {
@After
public void close() {
System.clearProperty("expected.name");
if (context != null) {
context.close();
}
@@ -53,21 +57,26 @@ public class BootstrapConfigurationTests {
@Test
public void picksUpAdditionalPropertySource() {
System.setProperty("expected.name","app");
context = new SpringApplicationBuilder().web(false)
.sources(BareConfiguration.class).run();
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
assertTrue(context.getEnvironment().getPropertySources().contains("bootstrap"));
assertNotNull(context.getBean(ConfigClientProperties.class));
}
@Test
public void applicationNameIsNotBootstrap() {
System.setProperty("expected.name","app");
context = new SpringApplicationBuilder().web(false)
.properties("spring.cloud.bootstrap.name:other")
.sources(BareConfiguration.class).run();
assertEquals("main",
assertEquals("app",
context.getEnvironment().getProperty("spring.application.name"));
// The parent is called "main" because spring.application.name is specified in
// other.properties (the bootstrap properties)
assertEquals(
"other",
"main",
context.getParent().getEnvironment()
.getProperty("spring.application.name"));
// The bootstrap context has a different "bootstrap" property source
@@ -77,7 +86,25 @@ public class BootstrapConfigurationTests {
}
@Test
public void environmentEnrichedOnceWhenShared() {
public void applicationNameNotInBootstrap() {
System.setProperty("expected.name","main");
context = new SpringApplicationBuilder()
.web(false)
.properties("spring.cloud.bootstrap.name:plain",
"spring.config.name:other").sources(BareConfiguration.class)
.run();
assertEquals("main",
context.getEnvironment().getProperty("spring.application.name"));
// The parent is called "plain" because spring.application.name is specified in
// other.properties (the application properties this time)
assertEquals(
"plain",
context.getParent().getEnvironment()
.getProperty("spring.application.name"));
}
@Test
public void environmentEnrichedOnceWhenSharedWithChildContext() {
context = new SpringApplicationBuilder().sources(BareConfiguration.class)
.environment(new StandardEnvironment()).child(BareConfiguration.class)
.web(false).run();
@@ -90,7 +117,7 @@ public class BootstrapConfigurationTests {
}
@Test
public void environmentEnrichedInParent() {
public void environmentEnrichedInParentContext() {
context = new SpringApplicationBuilder().sources(BareConfiguration.class)
.child(BareConfiguration.class).web(false).run();
assertEquals("bar", context.getEnvironment().getProperty("bootstrap.foo"));
@@ -132,14 +159,28 @@ public class BootstrapConfigurationTests {
}
@Configuration
@ConfigurationProperties("expected")
// This is added to bootstrap context as a source in bootstrap.properties
protected static class PropertySourceConfiguration implements PropertySourceLocator {
private String name;
@Override
public PropertySource<?> locate() {
public PropertySource<?> locate(Environment environment) {
if (name != null) {
assertEquals(name, environment.getProperty("spring.application.name"));
}
return new MapPropertySource("testBootstrap",
Collections.<String, Object> singletonMap("bootstrap.foo", "bar"));
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -15,17 +15,21 @@
*/
package org.springframework.cloud.config.client;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
/**
* @author Dave Syer
*
*/
public class ConfigServicePropertySourceLocatorTests {
private ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator();
public class ConfigClientPropertiesTests {
private ConfigClientProperties locator = new ConfigClientProperties(
new StandardEnvironment());
@Test
public void vanilla() {
@@ -45,7 +49,7 @@ public class ConfigServicePropertySourceLocatorTests {
}
@Test
public void overridePassword() {
public void explicitPassword() {
locator.setUri("http://foo:bar@localhost:9999");
locator.setPassword("secret");
assertEquals("http://localhost:9999", locator.getUri());
@@ -53,4 +57,13 @@ public class ConfigServicePropertySourceLocatorTests {
assertEquals("secret", locator.getPassword());
}
@Test
public void changeNameInOverride() {
locator.setName("one");
ConfigurableEnvironment environment = new StandardEnvironment();
EnvironmentTestUtils.addEnvironment(environment, "spring.application.name:two");
ConfigClientProperties override = locator.override(environment);
assertEquals("two", override.getName());
}
}

View File

@@ -1,3 +1,4 @@
spring.application.name: app
message: Hello scope!
delay: 0
# debug: true