diff --git a/pom.xml b/pom.xml
index 8c7b7b738..bb00e4721 100644
--- a/pom.xml
+++ b/pom.xml
@@ -134,6 +134,26 @@
spring-cloud-test-support
${spring-cloud-commons.version}
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+
+
+ org.testcontainers
+ mockserver
+ ${testcontainers.version}
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+
+
+ org.mock-server
+ mockserver-client-java
+ ${mockserverclient.version}
+
@@ -142,6 +162,8 @@
3.1.7-SNAPSHOT
3.1.8-SNAPSHOT
3.1.8-SNAPSHOT
+ 1.17.6
+ 5.15.0
diff --git a/spring-cloud-zookeeper-discovery/pom.xml b/spring-cloud-zookeeper-discovery/pom.xml
index a60e5ecc2..66e26ca38 100644
--- a/spring-cloud-zookeeper-discovery/pom.xml
+++ b/spring-cloud-zookeeper-discovery/pom.xml
@@ -156,6 +156,26 @@
reactor-test
test
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ mockserver
+ test
+
+
+ org.mock-server
+ mockserver-client-java
+ test
+
diff --git a/spring-cloud-zookeeper-discovery/src/main/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapper.java b/spring-cloud-zookeeper-discovery/src/main/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapper.java
index 5a67f7fca..5bdd11f4b 100644
--- a/spring-cloud-zookeeper-discovery/src/main/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapper.java
+++ b/spring-cloud-zookeeper-discovery/src/main/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapper.java
@@ -17,23 +17,31 @@
package org.springframework.cloud.zookeeper.discovery.configclient;
import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+import org.apache.commons.logging.Log;
+import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.x.discovery.ServiceDiscovery;
import org.apache.curator.x.discovery.ServiceDiscoveryBuilder;
import org.apache.curator.x.discovery.details.InstanceSerializer;
import org.apache.curator.x.discovery.details.JsonInstanceSerializer;
+import org.springframework.boot.BootstrapContext;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistryInitializer;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.commons.util.InetUtilsProperties;
import org.springframework.cloud.config.client.ConfigClientProperties;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.zookeeper.CuratorFactory;
+import org.springframework.cloud.zookeeper.ZookeeperProperties;
import org.springframework.cloud.zookeeper.discovery.ConditionalOnZookeeperDiscoveryEnabled;
import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryClient;
import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryProperties;
@@ -45,6 +53,12 @@
public class ZookeeperConfigServerBootstrapper implements BootstrapRegistryInitializer {
+ private static boolean isEnabled(Binder binder) {
+ return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false) &&
+ binder.bind(ConditionalOnZookeeperDiscoveryEnabled.PROPERTY, Boolean.class).orElse(true) &&
+ binder.bind("spring.cloud.discovery.enabled", Boolean.class).orElse(true);
+ }
+
@Override
@SuppressWarnings("unchecked")
public void initialize(BootstrapRegistry registry) {
@@ -95,7 +109,7 @@ public void initialize(BootstrapRegistry registry) {
}
ServiceDiscovery serviceDiscovery = context.get(ServiceDiscovery.class);
ZookeeperDependencies dependencies = binder.bind(ZookeeperDependencies.PREFIX, Bindable
- .of(ZookeeperDependencies.class), getBindHandler(context))
+ .of(ZookeeperDependencies.class), getBindHandler(context))
.orElseGet(ZookeeperDependencies::new);
ZookeeperDiscoveryProperties discoveryProperties = context.get(ZookeeperDiscoveryProperties.class);
@@ -103,12 +117,9 @@ public void initialize(BootstrapRegistry registry) {
});
// create instance provider
- registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> {
- if (!isEnabled(context.get(Binder.class))) {
- return (id) -> Collections.emptyList();
- }
- return context.get(ZookeeperDiscoveryClient.class)::getInstances;
- });
+ // We need to pass the lambda here so we do not create a new instance of ConfigServerInstanceProvider.Function
+ // which would result in a ClassNotFoundException when Spring Cloud Config is not on the classpath
+ registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, ZookeeperFunction::create);
// promote beans to context
registry.addCloseListener(event -> {
@@ -124,10 +135,63 @@ private BindHandler getBindHandler(org.springframework.boot.BootstrapContext con
return context.getOrElse(BindHandler.class, null);
}
- private boolean isEnabled(Binder binder) {
- return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false) &&
- binder.bind(ConditionalOnZookeeperDiscoveryEnabled.PROPERTY, Boolean.class).orElse(true) &&
- binder.bind("spring.cloud.discovery.enabled", Boolean.class).orElse(true);
+ /*
+ * This Function is executed when loading config data. Because of this we cannot rely on the
+ * BootstrapContext because Boot has not finished loading all the configuration data so if we
+ * ask the BootstrapContext for configuration data it will not have it. The apply method in this function
+ * is passed the Binder and BindHandler from the config data context which has the configuration properties that
+ * have been loaded so far in the config data process.
+ *
+ * We will create many of the same beans in this function as we do above in the initializer above. We do both
+ * to maintain compatibility since we are promoting those beans to the main application context.
+ */
+ static final class ZookeeperFunction implements ConfigServerInstanceProvider.Function {
+
+ private final BootstrapContext context;
+
+ private ZookeeperFunction(BootstrapContext context) {
+ this.context = context;
+ }
+
+ static ZookeeperFunction create(BootstrapContext context) {
+ return new ZookeeperFunction(context);
+ }
+
+ @Override
+ public List apply(String serviceId) {
+ return apply(serviceId, null, null, null);
+ }
+
+ @Override
+ public List apply(String serviceId, Binder binder, BindHandler bindHandler, Log log) {
+ if (binder == null || !isEnabled(binder)) {
+ return Collections.emptyList();
+ }
+
+ ZookeeperProperties properties = binder.bind(ZookeeperProperties.PREFIX, Bindable.of(ZookeeperProperties.class))
+ .orElse(new ZookeeperProperties());
+ RetryPolicy retryPolicy = new ExponentialBackoffRetry(properties.getBaseSleepTimeMs(), properties.getMaxRetries(),
+ properties.getMaxSleepMs());
+ try {
+ CuratorFramework curatorFramework = CuratorFactory.curatorFramework(properties, retryPolicy, Stream::of,
+ () -> null, () -> null);
+ InstanceSerializer serializer = new JsonInstanceSerializer<>(ZookeeperInstance.class);
+ ZookeeperDiscoveryProperties discoveryProperties = binder.bind(ZookeeperDiscoveryProperties.PREFIX, Bindable
+ .of(ZookeeperDiscoveryProperties.class), bindHandler)
+ .orElseGet(() -> new ZookeeperDiscoveryProperties(new InetUtils(new InetUtilsProperties())));
+ DefaultServiceDiscoveryCustomizer customizer = new DefaultServiceDiscoveryCustomizer(curatorFramework, discoveryProperties, serializer);
+ ServiceDiscovery serviceDiscovery = customizer.customize(ServiceDiscoveryBuilder.builder(ZookeeperInstance.class));
+ ZookeeperDependencies dependencies = binder.bind(ZookeeperDependencies.PREFIX, Bindable
+ .of(ZookeeperDependencies.class), bindHandler)
+ .orElseGet(ZookeeperDependencies::new);
+ return new ZookeeperDiscoveryClient(serviceDiscovery, dependencies, discoveryProperties).getInstances(serviceId);
+ }
+ catch (Exception e) {
+ log.warn("Error fetching config server instance from Zookeeper", e);
+ return Collections.emptyList();
+ }
+
+ }
}
}
diff --git a/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTests.java b/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTests.java
index a66ca956b..d1dd19ba8 100644
--- a/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTests.java
+++ b/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTests.java
@@ -18,6 +18,7 @@
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.commons.logging.Log;
import org.apache.curator.framework.CuratorFramework;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@@ -31,6 +32,7 @@
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.cloud.config.client.ConfigServerInstanceProvider;
import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryClient;
@@ -39,6 +41,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
public class ZookeeperConfigServerBootstrapperTests {
@@ -59,7 +62,8 @@ public void notEnabledReturnsEmptyList() {
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
- assertThat(providerFn.apply("id")).as("ConfigServerInstanceProvider.Function should return empty list")
+ Log log = mock(Log.class);
+ assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class), event.getBootstrapContext().get(BindHandler.class), log)).as("ConfigServerInstanceProvider.Function should return empty list")
.isEmpty();
})).run();
CuratorFramework curatorFramework = context.getBean("curatorFramework", CuratorFramework.class);
@@ -80,7 +84,8 @@ public void zookeeperDiscoveryClientDisabledReturnsEmptyList() {
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
- assertThat(providerFn.apply("id")).as("ConfigServerInstanceProvider.Function should return empty list")
+ Log log = mock(Log.class);
+ assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class), event.getBootstrapContext().get(BindHandler.class), log)).as("ConfigServerInstanceProvider.Function should return empty list")
.isEmpty();
})).run();
CuratorFramework curatorFramework = context.getBean("curatorFramework", CuratorFramework.class);
@@ -101,7 +106,7 @@ public void discoveryClientDisabledReturnsEmptyList() {
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
- assertThat(providerFn.apply("id")).as("ConfigServerInstanceProvider.Function should return empty list")
+ assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class), event.getBootstrapContext().get(BindHandler.class), mock(Log.class))).as("ConfigServerInstanceProvider.Function should return empty list")
.isEmpty();
})).run();
CuratorFramework curatorFramework = context.getBean("curatorFramework", CuratorFramework.class);
@@ -124,7 +129,7 @@ public void enabledAddsInstanceProviderFn() {
.addBootstrapRegistryInitializer(registry -> registry.addCloseListener(event -> {
ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext()
.get(ConfigServerInstanceProvider.Function.class);
- assertThat(providerFn.apply("id")).as("Should return empty list.")
+ assertThat(providerFn.apply("id", event.getBootstrapContext().get(Binder.class), event.getBootstrapContext().get(BindHandler.class), mock(Log.class))).as("Should return empty list.")
.isNotNull();
bootstrapDiscoveryClient.set(event.getBootstrapContext().get(ZookeeperDiscoveryClient.class));
})).run();
diff --git a/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTestsIT.java b/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTestsIT.java
new file mode 100644
index 000000000..2bd6ed462
--- /dev/null
+++ b/spring-cloud-zookeeper-discovery/src/test/java/org/springframework/cloud/zookeeper/discovery/configclient/ZookeeperConfigServerBootstrapperTestsIT.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2015-2022 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.discovery.configclient;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.x.discovery.ServiceDiscovery;
+import org.apache.curator.x.discovery.ServiceDiscoveryBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.client.MockServerClient;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.MockServerContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.environment.PropertySource;
+import org.springframework.cloud.zookeeper.ZookeeperProperties;
+import org.springframework.cloud.zookeeper.discovery.ZookeeperDiscoveryProperties;
+import org.springframework.cloud.zookeeper.discovery.ZookeeperInstance;
+import org.springframework.cloud.zookeeper.serviceregistry.ServiceInstanceRegistration;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+/**
+ * @author Ryan Baxter
+ */
+@Testcontainers
+class ZookeeperConfigServerBootstrapperTestsIT {
+ private static final int ZOOKEEPER_PORT = 2181;
+
+ public static final DockerImageName MOCKSERVER_IMAGE = DockerImageName.parse("mockserver/mockserver")
+ .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion());
+
+ @Container
+ private static final GenericContainer> zookeeper = new GenericContainer<>("zookeeper:3.8.0")
+ .withExposedPorts(ZOOKEEPER_PORT);
+
+ @Container
+ static MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE);
+
+ private ConfigurableApplicationContext context;
+
+ private ServiceDiscovery serviceDiscovery;
+
+ private CuratorFramework curatorFramework;
+
+ @BeforeEach
+ void before() {
+ curatorFramework = CuratorFrameworkFactory.builder().connectString(zookeeper.getHost() + ":" + zookeeper.getMappedPort(ZOOKEEPER_PORT))
+ .retryPolicy(new RetryOneTime(100)).build();
+
+ try {
+ curatorFramework.start();
+ serviceDiscovery = ServiceDiscoveryBuilder.builder(ZookeeperInstance.class).client(curatorFramework).basePath("/services")
+ .build();
+ serviceDiscovery.start();
+ serviceDiscovery.registerService(ServiceInstanceRegistration.builder().id("zookeeper-configserver").name("zookeeper-configserver")
+ .address(mockServer.getHost()).port(mockServer.getServerPort()).uriSpec(ZookeeperDiscoveryProperties.DEFAULT_URI_SPEC).build().getServiceInstance());
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @AfterEach
+ void after() throws IOException {
+ this.context.close();
+ this.serviceDiscovery.close();
+ this.curatorFramework.close();
+ }
+
+ @Test
+ public void contextLoads() throws JsonProcessingException {
+ Environment environment = new Environment("test", "default");
+ Map properties = new HashMap<>();
+ properties.put("hello", "world");
+ PropertySource p = new PropertySource("p1", properties);
+ environment.add(p);
+ ObjectMapper objectMapper = new ObjectMapper();
+ try (MockServerClient mockServerClient = new MockServerClient(mockServer.getHost(),
+ mockServer.getMappedPort(MockServerContainer.PORT))) {
+ mockServerClient.when(request().withPath("/application/default"))
+ .respond(response().withBody(objectMapper.writeValueAsString(environment))
+ .withHeader("content-type", "application/json"));
+ this.context = setup().run();
+ assertThat(this.context.getEnvironment().getProperty("hello")).isEqualTo("world");
+ }
+
+ }
+
+ SpringApplicationBuilder setup(String... env) {
+ return new SpringApplicationBuilder(TestConfig.class)
+ .properties(addDefaultEnv(env));
+ }
+
+ private String[] addDefaultEnv(String[] env) {
+ Set set = new LinkedHashSet<>();
+ if (env != null && env.length > 0) {
+ set.addAll(Arrays.asList(env));
+ }
+ set.add("spring.config.import=classpath:bootstrapper.yaml");
+ set.add("spring.cloud.config.enabled=true");
+ set.add("spring.cloud.service-registry.auto-registration.enabled=false");
+ set.add(ZookeeperProperties.PREFIX + ".connectString=" + zookeeper.getHost() + ":" + zookeeper.getMappedPort(ZOOKEEPER_PORT));
+ return set.toArray(new String[0]);
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class TestConfig {
+
+ }
+}
diff --git a/spring-cloud-zookeeper-discovery/src/test/resources/bootstrapper.yaml b/spring-cloud-zookeeper-discovery/src/test/resources/bootstrapper.yaml
new file mode 100644
index 000000000..7cde1d1b5
--- /dev/null
+++ b/spring-cloud-zookeeper-discovery/src/test/resources/bootstrapper.yaml
@@ -0,0 +1,8 @@
+spring:
+ config:
+ import: "optional:configserver:"
+ cloud:
+ config:
+ discovery:
+ service-id: zookeeper-configserver
+ enabled: true