From abed38689e146e5d9ab101e7c103bc5189f034bd Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Tue, 8 Sep 2020 15:38:03 -0400 Subject: [PATCH] Support for new Spring Boot ConfigData framework. (#265) --- .../ZookeeperConfigAutoConfiguration.java | 19 +- .../config/ZookeeperConfigDataLoader.java | 51 +++++ .../config/ZookeeperConfigDataLocation.java | 85 +++++++ .../ZookeeperConfigDataLocationResolver.java | 159 +++++++++++++ .../config/ZookeeperConfigProperties.java | 7 +- .../main/resources/META-INF/spring.factories | 8 + ...ZookeeperConfigAutoConfigurationTests.java | 12 +- .../ZookeeperConfigDataIntegrationTests.java | 215 ++++++++++++++++++ ...ConfigDataNotOptionalIntegrationTests.java | 74 ++++++ ...perPropertySourceLocatorFailFastTests.java | 6 +- .../cloud/zookeeper/ZookeeperProperties.java | 6 +- src/checkstyle/checkstyle-suppressions.xml | 7 + 12 files changed, 638 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java create mode 100644 spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocation.java create mode 100644 spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java create mode 100644 spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataIntegrationTests.java create mode 100644 spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataNotOptionalIntegrationTests.java create mode 100644 src/checkstyle/checkstyle-suppressions.xml diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfiguration.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfiguration.java index 8822eaf0..38b33d51 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfiguration.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfiguration.java @@ -16,14 +16,20 @@ package org.springframework.cloud.zookeeper.config; +import java.util.Collections; +import java.util.List; + import org.apache.curator.framework.CuratorFramework; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.endpoint.RefreshEndpoint; import org.springframework.cloud.zookeeper.ConditionalOnZookeeperEnabled; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; /** * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration @@ -39,15 +45,24 @@ public class ZookeeperConfigAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RefreshEndpoint.class) + @ConditionalOnProperty(name = "spring.cloud.zookeeper.config.watcher.enabled", matchIfMissing = true) protected static class ZkRefreshConfiguration { @Bean - @ConditionalOnProperty(name = "spring.cloud.zookeeper.config.watcher.enabled", matchIfMissing = true) - public ConfigWatcher configWatcher(ZookeeperPropertySourceLocator locator, + @ConditionalOnBean(ZookeeperPropertySourceLocator.class) + public ConfigWatcher propertySourceLocatorConfigWatcher(ZookeeperPropertySourceLocator locator, CuratorFramework curator) { return new ConfigWatcher(locator.getContexts(), curator); } + @Bean + @ConditionalOnMissingBean(ZookeeperPropertySourceLocator.class) + public ConfigWatcher configDataConfigWatcher(CuratorFramework curator, Environment env) { + List contexts = env.getProperty("spring.cloud.zookeeper.config.property-source-contexts", + List.class, Collections.emptyList()); + return new ConfigWatcher(contexts, curator); + } + } } diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java new file mode 100644 index 00000000..d87d8cc7 --- /dev/null +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLoader.java @@ -0,0 +1,51 @@ +/* + * 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataLoader; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; + +public class ZookeeperConfigDataLoader implements ConfigDataLoader { + + private static final Log log = LogFactory.getLog(ZookeeperPropertySourceLocator.class); + + @Override + public ConfigData load(ConfigDataLoaderContext context, ZookeeperConfigDataLocation location) { + try { + ZookeeperPropertySource propertySource = new ZookeeperPropertySource(location.getContext(), + location.getCurator()); + return new ConfigData(Collections.singletonList(propertySource)); + } + catch (Exception e) { + if (location.getProperties().isFailFast() || !location.isOptional()) { + throw new ConfigDataLocationNotFoundException(location, e); + } + else { + log.warn("Unable to load zookeeper config from " + location.getContext(), e); + } + } + return null; + } + +} diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocation.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocation.java new file mode 100644 index 00000000..48dd3b69 --- /dev/null +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocation.java @@ -0,0 +1,85 @@ +/* + * 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.Objects; + +import org.apache.curator.framework.CuratorFramework; + +import org.springframework.boot.context.config.ConfigDataLocation; +import org.springframework.core.style.ToStringCreator; + +public class ZookeeperConfigDataLocation extends ConfigDataLocation { + + private final CuratorFramework curator; + private final ZookeeperConfigProperties properties; + private final String context; + private final boolean optional; + + public ZookeeperConfigDataLocation(CuratorFramework curator, ZookeeperConfigProperties properties, String context, boolean optional) { + this.curator = curator; + this.properties = properties; + this.context = context; + this.optional = optional; + } + + public CuratorFramework getCurator() { + return this.curator; + } + + public ZookeeperConfigProperties getProperties() { + return this.properties; + } + + public String getContext() { + return this.context; + } + + public boolean isOptional() { + return this.optional; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ZookeeperConfigDataLocation that = (ZookeeperConfigDataLocation) o; + return this.curator.equals(that.curator) && + this.properties.equals(that.properties) && + this.optional == that.optional && + this.context.equals(that.context); + } + + @Override + public int hashCode() { + return Objects.hash(this.curator, this.properties, this.context); + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("context", context) + .append("optional", optional) + .append("properties", properties) + .toString(); + + } +} diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java new file mode 100644 index 00000000..1f06a6ea --- /dev/null +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataLocationResolver.java @@ -0,0 +1,159 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; + +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; +import org.springframework.boot.context.config.ConfigDataLocationResolver; +import org.springframework.boot.context.config.ConfigDataLocationResolverContext; +import org.springframework.boot.context.config.Profiles; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration; +import org.springframework.cloud.zookeeper.ZookeeperProperties; +import org.springframework.core.env.MapPropertySource; + +public class ZookeeperConfigDataLocationResolver implements ConfigDataLocationResolver { + + private static final Log log = LogFactory.getLog(ZookeeperAutoConfiguration.class); + + @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; + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, String location, boolean optional) throws ConfigDataLocationNotFoundException { + return Collections.emptyList(); + } + + @Override + public List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional, Profiles profiles) throws ConfigDataLocationNotFoundException { + // TODO use location for host:port + CuratorFramework curator = curatorFramework(optional, loadProperties(context.getBinder())); + + String appName = context.getBinder().bind("spring.application.name", String.class).orElse("application"); + + ZookeeperConfigProperties properties = loadConfigProperties(context.getBinder()); + String root = properties.getRoot(); + List 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); + + context.getBootstrapRegistry().register(CuratorFramework.class, () -> curator) + .onApplicationContextPrepared((ctxt, curatorFramework) -> { + ctxt.getBeanFactory().registerSingleton("configDataCuratorFramework", curatorFramework); + HashMap source = new HashMap<>(); + source.put("spring.cloud.zookeeper.config.property-source-contexts", contexts); + MapPropertySource propertySource = new MapPropertySource("zookeeperConfigData", source); + ctxt.getEnvironment().getPropertySources().addFirst(propertySource); + }); + + ArrayList locations = new ArrayList<>(); + contexts.forEach(propertySourceContext -> locations + .add(new ZookeeperConfigDataLocation(curator, properties, propertySourceContext, optional))); + + return locations; + } + + private void addProfiles(List contexts, String baseContext, Profiles profiles, + ZookeeperConfigProperties properties) { + for (String profile : profiles.getAccepted()) { + contexts.add(baseContext + properties.getProfileSeparator() + profile); + } + } + + protected CuratorFramework curatorFramework(boolean optional, ZookeeperProperties properties) { + CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder(); + + builder.connectString(properties.getConnectString()) + .sessionTimeoutMs((int) properties.getSessionTimeout().toMillis()) + .connectionTimeoutMs((int) properties.getConnectionTimeout().toMillis()) + .retryPolicy(retryPolicy(properties)); + + CuratorFramework curator = builder.build(); + + curator.start(); + if (log.isTraceEnabled()) { + log.trace("blocking until connected to zookeeper for " + properties.getBlockUntilConnectedWait() + + properties.getBlockUntilConnectedUnit()); + } + try { + curator.blockUntilConnected(properties.getBlockUntilConnectedWait(), + properties.getBlockUntilConnectedUnit()); + } + catch (InterruptedException e) { + if (!optional) { + log.error("Unable to connect to zookeeper", e); + throw new ConfigDataLocationNotFoundException("Unable to connect to zookeeper", null, e); + } + else if (log.isDebugEnabled()) { + log.debug("Unable to connect to zookeeper", e); + } + } + if (log.isTraceEnabled()) { + log.trace("connected to zookeeper"); + } + return curator; + } + + protected RetryPolicy retryPolicy(ZookeeperProperties properties) { + return new ExponentialBackoffRetry(properties.getBaseSleepTimeMs(), properties.getMaxRetries(), + properties.getMaxSleepMs()); + } + + protected ZookeeperProperties loadProperties(Binder binder) { + ZookeeperProperties properties = binder.bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class)) + .orElse(new ZookeeperProperties()); + return properties; + } + + protected ZookeeperConfigProperties loadConfigProperties(Binder binder) { + ZookeeperConfigProperties properties = binder + .bind(ZookeeperConfigProperties.PREFIX, Bindable.of(ZookeeperConfigProperties.class)) + .orElse(new ZookeeperConfigProperties()); + return properties; + } + +} diff --git a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java index 776a08b0..b14bba28 100644 --- a/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java +++ b/spring-cloud-zookeeper-config/src/main/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigProperties.java @@ -26,9 +26,14 @@ import org.springframework.util.Assert; * @since 1.0.0 * @see ZookeeperPropertySourceLocator */ -@ConfigurationProperties("spring.cloud.zookeeper.config") +@ConfigurationProperties(ZookeeperConfigProperties.PREFIX) public class ZookeeperConfigProperties { + /** + * Configuration prefix for config properties. + */ + public static final String PREFIX = "spring.cloud.zookeeper.config"; + private boolean enabled = true; /** diff --git a/spring-cloud-zookeeper-config/src/main/resources/META-INF/spring.factories b/spring-cloud-zookeeper-config/src/main/resources/META-INF/spring.factories index f9be33b4..bf6df9bd 100644 --- a/spring-cloud-zookeeper-config/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-zookeeper-config/src/main/resources/META-INF/spring.factories @@ -5,3 +5,11 @@ org.springframework.cloud.zookeeper.config.ZookeeperConfigAutoConfiguration # Bootstrap Configuration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.zookeeper.config.ZookeeperConfigBootstrapConfiguration + +# ConfigData Location Resolvers +org.springframework.boot.context.config.ConfigDataLocationResolver=\ +org.springframework.cloud.zookeeper.config.ZookeeperConfigDataLocationResolver + +# ConfigData Loaders +org.springframework.boot.context.config.ConfigDataLoader=\ +org.springframework.cloud.zookeeper.config.ZookeeperConfigDataLoader diff --git a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfigurationTests.java b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfigurationTests.java index 52cfba8e..7da3c213 100644 --- a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfigurationTests.java +++ b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigAutoConfigurationTests.java @@ -16,8 +16,8 @@ package org.springframework.cloud.zookeeper.config; -import java.net.ConnectException; - +import org.apache.zookeeper.KeeperException; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -25,8 +25,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; @@ -72,7 +73,7 @@ public class ZookeeperConfigAutoConfigurationTests { @Test public void testConfigEnabledTrueLoadsZookeeperConfigAutoConfiguration() throws Exception { - expectedException.expect(ConnectException.class); + expectedException.expectCause(Matchers.isA(KeeperException.class)); new SpringApplicationBuilder().sources(Config.class).web(WebApplicationType.NONE) .run("--spring.application.name=testZookeeperConfigEnabledSetToTrue", @@ -87,7 +88,8 @@ public class ZookeeperConfigAutoConfigurationTests { "--spring.cloud.zookeeper.config.enabled=true"); } - @SpringBootApplication + @SpringBootConfiguration + @EnableAutoConfiguration static class Config { } diff --git a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataIntegrationTests.java b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataIntegrationTests.java new file mode 100644 index 00000000..87abdca3 --- /dev/null +++ b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataIntegrationTests.java @@ -0,0 +1,215 @@ +/* + * Copyright 2015-2019 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.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.RetryOneTime; +import org.apache.curator.test.TestingServer; +import org.apache.zookeeper.KeeperException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Spencer Gibb + */ +public class ZookeeperConfigDataIntegrationTests { + + private static final Log log = LogFactory + .getLog(ZookeeperConfigDataIntegrationTests.class); + + public static final String PREFIX = "test__configdata__"; + + public static final String ROOT = "/" + PREFIX + UUID.randomUUID(); + + public static final String CONTEXT = ROOT + "/application/"; + + public static final String KEY_BASIC = "testProp"; + + public static final String KEY_BASIC_PATH = CONTEXT + KEY_BASIC; + + public static final String VAL_BASIC = "testPropVal"; + + public static final String KEY_WITH_DOT = "testProp.dot"; + + public static final String KEY_WITH_DOT_PATH = CONTEXT + KEY_WITH_DOT; + + public static final String VAL_WITH_DOT = "withDotVal"; + + public static final String KEY_NESTED = "testProp.nested"; + + public static final String KEY_NESTED_PATH = CONTEXT + KEY_NESTED.replace('.', '/'); + + public static final String VAL_NESTED = "nestedVal"; + + public static final String KEY_WITHOUT_VALUE = "testProp.novalue"; + + public static final String KEY_WITHOUT_VALUE_PATH = CONTEXT + KEY_WITHOUT_VALUE; + + private ConfigurableEnvironment environment; + + private ConfigurableApplicationContext context; + + private TestingServer testingServer; + + private CuratorFramework curator; + + @Before + public void setup() throws Exception { + int port = SocketUtils.findAvailableTcpPort(); + this.testingServer = new TestingServer(port); + String connectString = "localhost:" + port; + this.curator = CuratorFrameworkFactory.builder() + .retryPolicy(new RetryOneTime(500)).connectString(connectString).build(); + this.curator.start(); + + List children = this.curator.getChildren().forPath("/"); + for (String child : children) { + if (child.startsWith(PREFIX) && child.length() > PREFIX.length()) { + delete("/" + child); + } + } + + StringBuilder create = new StringBuilder(1024); + create.append(this.curator.create().creatingParentsIfNeeded() + .forPath(KEY_BASIC_PATH, VAL_BASIC.getBytes())).append('\n'); + create.append(this.curator.create().creatingParentsIfNeeded() + .forPath(KEY_WITH_DOT_PATH, VAL_WITH_DOT.getBytes())).append('\n'); + create.append(this.curator.create().creatingParentsIfNeeded() + .forPath(KEY_NESTED_PATH, VAL_NESTED.getBytes())).append('\n'); + create.append(this.curator.create().creatingParentsIfNeeded() + .forPath(KEY_WITHOUT_VALUE_PATH, null)).append('\n'); + this.curator.close(); + System.out.println(create); + + this.context = new SpringApplicationBuilder(Config.class) + .web(WebApplicationType.NONE) + .run("--spring.cloud.zookeeper.connectString=" + connectString, + "--debug=true", + //"--spring.cloud.bootstrap.enabled=false", + "--spring.config.import=zookeeper:", + "--spring.application.name=testZkConfigDataIntegration", + "--logging.level.org.springframework.cloud.zookeeper=DEBUG", + "--spring.cloud.zookeeper.config.root=" + ROOT); + + this.curator = this.context.getBean(CuratorFramework.class); + this.environment = this.context.getEnvironment(); + } + + public void delete(String path) throws Exception { + try { + this.curator.delete().deletingChildrenIfNeeded().forPath(path); + } + catch (KeeperException e) { + if (e.code() != KeeperException.Code.NONODE) { + throw e; + } + } + } + + @After + public void after() throws Exception { + try { + delete(ROOT); + } + finally { + this.context.close(); + this.testingServer.close(); + } + } + + @Test + public void checkKeyValues() throws Exception { + String propValue = this.environment.getProperty(KEY_BASIC); + assertThat(propValue).as(KEY_BASIC + " was wrong").isEqualTo(VAL_BASIC); + + propValue = this.environment.getProperty(KEY_NESTED); + assertThat(propValue).as(VAL_NESTED + " was wrong").isEqualTo(VAL_NESTED); + + propValue = this.environment.getProperty(KEY_WITH_DOT); + assertThat(propValue).as(VAL_WITH_DOT + " was wrong").isEqualTo(VAL_WITH_DOT); + + propValue = this.environment.getProperty(KEY_WITHOUT_VALUE); + assertThat(propValue).as(KEY_WITHOUT_VALUE + " was wrong").isEmpty(); + } + + @Test + public void propertyLoadedAndUpdated() throws Exception { + String testProp = this.environment.getProperty(KEY_BASIC); + assertThat(testProp).as("testProp was wrong").isEqualTo(VAL_BASIC); + + this.curator.setData().forPath(KEY_BASIC_PATH, "testPropValUpdate".getBytes()); + + CountDownLatch latch = this.context.getBean(CountDownLatch.class); + boolean receivedEvent = latch.await(15, TimeUnit.SECONDS); + assertThat(receivedEvent).as("listener didn't receive event").isTrue(); + + testProp = this.environment.getProperty(KEY_BASIC); + assertThat(testProp).as("testProp was wrong after update") + .isEqualTo("testPropValUpdate"); + } + + @Configuration + @EnableAutoConfiguration + static class Config implements ApplicationListener { + + @Bean + public CountDownLatch countDownLatch() { + return new CountDownLatch(1); + } + + @Bean + public ContextRefresher contextRefresher(ConfigurableApplicationContext context, + RefreshScope scope) { + return new ConfigDataContextRefresher(context, scope); + } + + @Override + public void onApplicationEvent(EnvironmentChangeEvent event) { + log.debug("Event keys: " + event.getKeys()); + if (event.getKeys().contains(KEY_BASIC)) { + countDownLatch().countDown(); + } + } + + } + +} diff --git a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataNotOptionalIntegrationTests.java b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataNotOptionalIntegrationTests.java new file mode 100644 index 00000000..9d1d1bd8 --- /dev/null +++ b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperConfigDataNotOptionalIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2019 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 org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.config.ConfigDataLocationNotFoundException; +import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Spencer Gibb + */ +public class ZookeeperConfigDataNotOptionalIntegrationTests { + + @Test + public void configDataNotFoundThrowsException() { + Assertions.assertThatThrownBy(() -> { + ConfigurableApplicationContext context = null; + try { + context = new SpringApplicationBuilder(Config.class).web(WebApplicationType.NONE).run( + "--spring.cloud.zookeeper.connect-string=notexistantdomain:5000", "--debug=true", + "--spring.cloud.zookeeper.max-retries=0", + "--spring.cloud.zookeeper.blockUntilConnectedWait=1", + "--spring.cloud.zookeeper.blockUntilConnectedUnit=MILLISECONDS", + "--spring.cloud.zookeeper.connection-timeout=1ms", + "--spring.config.import=zookeeper:", + "--spring.application.name=testZkConfigDataNotOptionalIntegration", + "--logging.level.org.springframework.cloud.zookeeper=DEBUG", + "--spring.cloud.zookeeper.config.root=/shouldfail"); + + } + finally { + if (context != null) { + context.close(); + } + } + }).isInstanceOf(ConfigDataLocationNotFoundException.class); + } + + @Configuration + @EnableAutoConfiguration + static class Config { + + @Bean + public ContextRefresher contextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { + return new ConfigDataContextRefresher(context, scope); + } + + } + +} diff --git a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocatorFailFastTests.java b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocatorFailFastTests.java index ad494911..f7fa13f6 100644 --- a/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocatorFailFastTests.java +++ b/spring-cloud-zookeeper-config/src/test/java/org/springframework/cloud/zookeeper/config/ZookeeperPropertySourceLocatorFailFastTests.java @@ -20,8 +20,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import static org.assertj.core.api.Assertions.assertThatCode; @@ -78,7 +79,8 @@ public class ZookeeperPropertySourceLocatorFailFastTests { .isNotNull(); } - @SpringBootApplication + @SpringBootConfiguration + @EnableAutoConfiguration static class Config { } diff --git a/spring-cloud-zookeeper-core/src/main/java/org/springframework/cloud/zookeeper/ZookeeperProperties.java b/spring-cloud-zookeeper-core/src/main/java/org/springframework/cloud/zookeeper/ZookeeperProperties.java index 9fee5421..9c13338c 100644 --- a/spring-cloud-zookeeper-core/src/main/java/org/springframework/cloud/zookeeper/ZookeeperProperties.java +++ b/spring-cloud-zookeeper-core/src/main/java/org/springframework/cloud/zookeeper/ZookeeperProperties.java @@ -30,9 +30,13 @@ import org.springframework.util.Assert; * @author Spencer Gibb * @since 1.0.0 */ -@ConfigurationProperties("spring.cloud.zookeeper") +@ConfigurationProperties(ZookeeperProperties.PREFIX) public class ZookeeperProperties { + /** + * Configuration prefix. + */ + public static final String PREFIX = "spring.cloud.zookeeper"; /** * Connection string to the Zookeeper cluster. */ diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml new file mode 100644 index 00000000..02a33e7b --- /dev/null +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file