Adds support for paths in ConfigData

Now `zookeeper:myhost:8501/mypath1;/mypath2;/mypath3` will use the paths separated by semicolon (;) instead of automatically generating paths. `zookeeper:myhost:8501` or `zookeeper:` will automatically generate paths from ZookeeperConfigProperties and active profiles.
This commit is contained in:
spencergibb
2020-09-09 23:25:49 -04:00
parent 62df7aa2d7
commit 2b759686d9
4 changed files with 231 additions and 48 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.zookeeper.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -36,19 +37,28 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.cloud.zookeeper.ZookeeperProperties;
import org.springframework.core.env.MapPropertySource;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationResolver<ZookeeperConfigDataLocation> {
private static final Log log = LogFactory.getLog(ZookeeperConfigDataLocationResolver.class);
/**
* Zookeeper Config Data prefix.
*/
public static final String PREFIX = "zookeeper:";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
boolean zkEnabled = context.getBinder().bind(ZookeeperProperties.PREFIX + ".enabled", Boolean.class)
.orElse(true);
boolean zkConfigEnabled = context.getBinder().bind(ZookeeperConfigProperties.PREFIX + ".enabled", Boolean.class)
.orElse(true);
return location.startsWith("zookeeper:") && zkConfigEnabled && zkEnabled;
return location.startsWith(PREFIX) && zkConfigEnabled && zkEnabled;
}
@Override
@@ -60,29 +70,16 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
@Override
public List<ZookeeperConfigDataLocation> resolveProfileSpecific(ConfigDataLocationResolverContext context,
String location, boolean optional, Profiles profiles) throws ConfigDataLocationNotFoundException {
String appName = context.getBinder().bind("spring.application.name", String.class).orElse("application");
UriComponents locationUri = parseLocation(context, location);
ZookeeperConfigProperties properties = loadConfigProperties(context.getBinder());
String root = properties.getRoot();
List<String> contexts = new ArrayList<>();
String defaultContext = root + "/" + properties.getDefaultContext();
contexts.add(defaultContext);
addProfiles(contexts, defaultContext, profiles, properties);
StringBuilder baseContext = new StringBuilder(root);
if (!appName.startsWith("/")) {
baseContext.append("/");
}
baseContext.append(appName);
contexts.add(baseContext.toString());
addProfiles(contexts, baseContext.toString(), profiles, properties);
Collections.reverse(contexts);
List<String> contexts = (locationUri == null || CollectionUtils.isEmpty(locationUri.getPathSegments()))
? getAutomaticContexts(profiles, properties) : getCustomContexts(locationUri, properties);
context.getBootstrapRegistry()
.register(CuratorFramework.class, () -> curatorFramework(optional, loadProperties(context, location)))
.register(CuratorFramework.class,
() -> curatorFramework(optional, loadProperties(context.getBinder(), locationUri)))
.onApplicationContextPrepared((ctxt, curatorFramework) -> {
ctxt.getBeanFactory().registerSingleton("configDataCuratorFramework", curatorFramework);
HashMap<String, Object> source = new HashMap<>();
@@ -98,6 +95,50 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
return locations;
}
protected List<String> getCustomContexts(UriComponents uriComponents, ZookeeperConfigProperties properties) {
if (StringUtils.isEmpty(uriComponents.getPath())) {
return Collections.emptyList();
}
return Arrays.asList(uriComponents.getPath().split(";"));
}
protected List<String> getAutomaticContexts(Profiles profiles, ZookeeperConfigProperties properties) {
String root = properties.getRoot();
List<String> contexts = new ArrayList<>();
String defaultContext = root + "/" + properties.getDefaultContext();
contexts.add(defaultContext);
addProfiles(contexts, defaultContext, profiles, properties);
StringBuilder baseContext = new StringBuilder(root);
if (!properties.getName().startsWith("/")) {
baseContext.append("/");
}
// getName() defaults to ${spring.application.name} or application
baseContext.append(properties.getName());
contexts.add(baseContext.toString());
addProfiles(contexts, baseContext.toString(), profiles, properties);
Collections.reverse(contexts);
return contexts;
}
@Nullable
protected UriComponents parseLocation(ConfigDataLocationResolverContext context, String location) {
String uri = location.substring(PREFIX.length());
if (!StringUtils.hasText(uri)) {
return null;
}
if (!uri.startsWith("//")) {
uri = PREFIX + "//" + uri;
}
else {
uri = location;
}
return UriComponentsBuilder.fromUriString(uri).build();
}
private void addProfiles(List<String> contexts, String baseContext, Profiles profiles,
ZookeeperConfigProperties properties) {
for (String profile : profiles.getAccepted()) {
@@ -144,18 +185,17 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
properties.getMaxSleepMs());
}
private ZookeeperProperties loadProperties(ConfigDataLocationResolverContext context, String location) {
ZookeeperProperties properties = context.getBinder()
.bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class))
protected ZookeeperProperties loadProperties(Binder binder, UriComponents location) {
ZookeeperProperties properties = binder.bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class))
.orElse(new ZookeeperProperties());
String connectString = location.substring("zookeeper:".length());
if (StringUtils.hasText(connectString)) {
properties.setConnectString(connectString);
if (location != null && StringUtils.hasText(location.getHost())) {
if (location.getPort() < 0) {
throw new IllegalArgumentException("zookeeper port must be greater than or equal to zero: " + location.getPort());
}
properties.setConnectString(location.getHost() + ":" + location.getPort());
}
// TODO: support ZookeeperConfigProperties.root
return properties;
}
@@ -163,6 +203,11 @@ public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationRe
ZookeeperConfigProperties properties = binder
.bind(ZookeeperConfigProperties.PREFIX, Bindable.of(ZookeeperConfigProperties.class))
.orElse(new ZookeeperConfigProperties());
if (StringUtils.isEmpty(properties.getName())) {
properties.setName(binder.bind("spring.application.name", String.class).orElse("application"));
}
return properties;
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.zookeeper.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
@@ -41,6 +42,11 @@ public class ZookeeperConfigProperties {
*/
private String root = "config";
/**
* Alternative to spring.application.name to use in looking up values in zookeeper.
*/
private String name;
/**
* The name of the default context.
*/
@@ -60,42 +66,62 @@ public class ZookeeperConfigProperties {
return this.enabled;
}
public String getRoot() {
return this.root;
}
public String getDefaultContext() {
return this.defaultContext;
}
public String getProfileSeparator() {
return this.profileSeparator;
}
public boolean isFailFast() {
return this.failFast;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getRoot() {
return this.root;
}
public void setRoot(String root) {
this.root = root;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDefaultContext() {
return this.defaultContext;
}
public void setDefaultContext(String defaultContext) {
Assert.hasText(defaultContext, "spring.cloud.zookeeper.config.default-context may not be empty");
this.defaultContext = defaultContext;
}
public String getProfileSeparator() {
return this.profileSeparator;
}
public void setProfileSeparator(String profileSeparator) {
Assert.hasText(profileSeparator, "spring.cloud.zookeeper.config.profile-separator may not be empty");
this.profileSeparator = profileSeparator;
}
public boolean isFailFast() {
return this.failFast;
}
public void setFailFast(boolean failFast) {
this.failFast = failFast;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("enabled", enabled)
.append("root", root)
.append("name", name)
.append("defaultContext", defaultContext)
.append("profileSeparator", profileSeparator)
.append("failFast", failFast)
.toString();
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Zookeeper provides a <a href=
@@ -88,12 +89,13 @@ public class ZookeeperPropertySourceLocator implements PropertySourceLocator {
public PropertySource<?> locate(Environment environment) {
if (environment instanceof ConfigurableEnvironment) {
ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
String appName = env.getProperty("spring.application.name");
if (appName == null) {
String appName = properties.getName();
if (StringUtils.isEmpty(appName)) {
// use default "application" (which config client does)
appName = "application";
log.warn(
"spring.application.name is not set. Using default of 'application'");
appName = env.getProperty("spring.application.name", "application");
if (appName.equals("application")) {
log.warn("spring.application.name is not set. Using default of 'application'");
}
}
List<String> profiles = Arrays.asList(env.getActiveProfiles());

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2015-2020 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
*
* https://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.zookeeper.config;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.Profiles;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.BootstrapRegistry;
import org.springframework.cloud.zookeeper.ZookeeperProperties;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ZookeeperConfigDataLocationResolverTests {
@Test
public void testParseLocation() {
ZookeeperConfigDataLocationResolver resolver = new ZookeeperConfigDataLocationResolver();
UriComponents uriComponents = resolver.parseLocation(null,
"zookeeper:myhost:2182/mypath1;/mypath2;/mypath3");
assertThat(uriComponents.toUri()).hasScheme("zookeeper").hasHost("myhost")
.hasPort(2182).hasPath("/mypath1;/mypath2;/mypath3");
uriComponents = resolver.parseLocation(null, "zookeeper:myhost:2182");
assertThat(uriComponents.toUri()).hasScheme("zookeeper").hasHost("myhost")
.hasPort(2182).hasPath("");
}
@Test
public void testResolveProfileSpecificWithCustomPaths() {
String location = "zookeeper:myhost:2182/mypath1;/mypath2;/mypath3";
List<ZookeeperConfigDataLocation> locations = testResolveProfileSpecific(location);
assertThat(locations).hasSize(3);
assertThat(toContexts(locations)).containsExactly("/mypath1", "/mypath2",
"/mypath3");
}
@Test
public void testResolveProfileSpecificWithAutomaticPaths() {
String location = "zookeeper:myhost";
List<ZookeeperConfigDataLocation> locations = testResolveProfileSpecific(location);
assertThat(locations).hasSize(4);
assertThat(toContexts(locations)).containsExactly("config/testapp,dev",
"config/testapp", "config/application,dev", "config/application");
}
@Test
public void testLoadProperties() {
ZookeeperProperties properties = createResolver().loadProperties(
Binder.get(new MockEnvironment()),
UriComponentsBuilder.fromUriString("zookeeper://myhost:8502").build());
assertThat(properties.getConnectString()).isEqualTo("myhost:8502");
}
private List<String> toContexts(List<ZookeeperConfigDataLocation> locations) {
return locations.stream().map(ZookeeperConfigDataLocation::getContext)
.collect(Collectors.toList());
}
private List<ZookeeperConfigDataLocation> testResolveProfileSpecific(String location) {
ZookeeperConfigDataLocationResolver resolver = createResolver();
MockEnvironment env = new MockEnvironment();
env.setProperty("spring.application.name", "testapp");
BootstrapRegistry registry = mock(BootstrapRegistry.class);
when(registry.register(any(), any())).thenReturn(mock(BootstrapRegistry.Registration.class));
ConfigDataLocationResolverContext context = mock(
ConfigDataLocationResolverContext.class);
when(context.getBootstrapRegistry()).thenReturn(registry);
when(context.getBinder()).thenReturn(Binder.get(env));
Profiles profiles = mock(Profiles.class);
when(profiles.getAccepted()).thenReturn(Collections.singletonList("dev"));
return resolver.resolveProfileSpecific(context, location, false, profiles);
}
private ZookeeperConfigDataLocationResolver createResolver() {
return new ZookeeperConfigDataLocationResolver();
}
}