diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java index 2d279d65d..e2e22d1ca 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapConfiguration.java @@ -16,18 +16,8 @@ package org.springframework.cloud.netflix.eureka.config; -import java.util.ArrayList; -import java.util.List; - -import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClientConfig; -import com.netflix.discovery.endpoint.EndpointUtils; -import com.netflix.discovery.shared.Applications; -import com.netflix.discovery.shared.resolver.DefaultEndpoint; import com.netflix.discovery.shared.transport.EurekaHttpClient; -import com.netflix.discovery.shared.transport.EurekaHttpResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -38,11 +28,9 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.config.client.ConfigServerInstanceProvider; import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator; import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; -import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.cloud.netflix.eureka.http.RestTemplateEurekaHttpClient; import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory; import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient; @@ -50,8 +38,6 @@ import org.springframework.cloud.netflix.eureka.http.WebClientTransportClientFac import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.http.HttpStatus; -import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.client.WebClient; /** @@ -61,13 +47,11 @@ import org.springframework.web.reactive.function.client.WebClient; * @author Dave Syer */ @ConditionalOnClass(ConfigServicePropertySourceLocator.class) -@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled", matchIfMissing = false) +@ConditionalOnProperty("spring.cloud.config.discovery.enabled") @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties public class EurekaConfigServerBootstrapConfiguration { - private static final Log log = LogFactory.getLog(EurekaConfigServerBootstrapConfiguration.class); - @Bean @ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT) public EurekaClientConfigBean eurekaClientConfigBean() { @@ -81,78 +65,13 @@ public class EurekaConfigServerBootstrapConfiguration { public RestTemplateEurekaHttpClient configDiscoveryRestTemplateEurekaHttpClient(EurekaClientConfigBean config, Environment env) { return (RestTemplateEurekaHttpClient) new RestTemplateTransportClientFactory() - .newClient(new DefaultEndpoint(getEurekaUrl(config, env))); - } - - private static String getEurekaUrl(EurekaClientConfigBean config, Environment env) { - List urls = EndpointUtils.getDiscoveryServiceUrls(config, EurekaClientConfigBean.DEFAULT_ZONE, - new HostnameBasedUrlRandomizer(env.getProperty("eureka.instance.hostname"))); - return urls.get(0); - } - - private boolean isSuccessful(EurekaHttpResponse response) { - HttpStatus httpStatus = HttpStatus.resolve(response.getStatusCode()); - return httpStatus != null && httpStatus.is2xxSuccessful(); + .newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, env)); } @Bean public ConfigServerInstanceProvider.Function eurekaConfigServerInstanceProvider(EurekaHttpClient client, EurekaClientConfig config) { - - return serviceId -> { - if (log.isDebugEnabled()) { - log.debug("eurekaConfigServerInstanceProvider finding instances for " + serviceId); - } - EurekaHttpResponse response = client.getApplications(config.getRegion()); - List instances = new ArrayList<>(); - if (!isSuccessful(response) || response.getEntity() == null) { - return instances; - } - - Applications applications = response.getEntity(); - applications.shuffleInstances(config.shouldFilterOnlyUpInstances()); - List infos = applications.getInstancesByVirtualHostName(serviceId); - for (InstanceInfo info : infos) { - instances.add(new EurekaServiceInstance(info)); - } - if (log.isDebugEnabled()) { - log.debug("eurekaConfigServerInstanceProvider found " + infos.size() + " instance(s) for " + serviceId - + ", " + instances); - } - return instances; - }; - } - - private static final class HostnameBasedUrlRandomizer implements EndpointUtils.ServiceUrlRandomizer { - - private final String hostname; - - private HostnameBasedUrlRandomizer(String hostname) { - this.hostname = hostname; - } - - @Override - public void randomize(List urlList) { - int listSize = 0; - if (urlList != null) { - listSize = urlList.size(); - } - if (!StringUtils.hasText(hostname) || listSize == 0) { - return; - } - // Find the hashcode of the instance hostname and use it to find an entry - // and then arrange the rest of the entries after this entry. - int instanceHashcode = hostname.hashCode(); - if (instanceHashcode < 0) { - instanceHashcode = instanceHashcode * -1; - } - int backupInstance = instanceHashcode % listSize; - for (int i = 0; i < backupInstance; i++) { - String zone = urlList.remove(0); - urlList.add(zone); - } - } - + return new EurekaConfigServerInstanceProvider(client, config)::getInstances; } @Configuration(proxyBeanMethods = false) @@ -166,7 +85,7 @@ public class EurekaConfigServerBootstrapConfiguration { public WebClientEurekaHttpClient configDiscoveryWebClientEurekaHttpClient(EurekaClientConfigBean config, ObjectProvider builder, Environment env) { return (WebClientEurekaHttpClient) new WebClientTransportClientFactory(builder::getIfAvailable) - .newClient(new DefaultEndpoint(getEurekaUrl(config, env))); + .newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, env)); } } diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapper.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapper.java new file mode 100644 index 000000000..6e99acaa1 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapper.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-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.netflix.eureka.config; + +import com.netflix.discovery.shared.transport.EurekaHttpClient; + +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.Bootstrapper; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.config.client.ConfigClientProperties; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.cloud.netflix.eureka.http.RestTemplateTransportClientFactory; + +public class EurekaConfigServerBootstrapper implements Bootstrapper { + + @Override + public void intitialize(BootstrapRegistry registry) { + registry.registerIfAbsent(EurekaClientConfigBean.class, context -> { + Binder binder = context.get(Binder.class); + if (!getDiscoveryEnabled(binder)) { + return null; + } + + return binder.bind(EurekaClientConfigBean.PREFIX, EurekaClientConfigBean.class) + .orElseGet(EurekaClientConfigBean::new); + }); + registry.registerIfAbsent(ConfigServerInstanceProvider.Function.class, context -> { + Binder binder = context.get(Binder.class); + if (!getDiscoveryEnabled(binder)) { + return null; + } + EurekaClientConfigBean config = context.get(EurekaClientConfigBean.class); + EurekaHttpClient httpClient = new RestTemplateTransportClientFactory() + .newClient(HostnameBasedUrlRandomizer.randomEndpoint(config, binder)); + return new EurekaConfigServerInstanceProvider(httpClient, config)::getInstances; + }); + } + + private Boolean getDiscoveryEnabled(Binder binder) { + return binder.bind(ConfigClientProperties.CONFIG_DISCOVERY_ENABLED, Boolean.class).orElse(false); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerInstanceProvider.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerInstanceProvider.java new file mode 100644 index 000000000..78769063e --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerInstanceProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-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.netflix.eureka.config; + +import java.util.ArrayList; +import java.util.List; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.discovery.shared.Applications; +import com.netflix.discovery.shared.transport.EurekaHttpClient; +import com.netflix.discovery.shared.transport.EurekaHttpResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; +import org.springframework.http.HttpStatus; + +public class EurekaConfigServerInstanceProvider { + + private final Log log; + + private final EurekaHttpClient client; + + private final EurekaClientConfig config; + + public EurekaConfigServerInstanceProvider(EurekaHttpClient client, EurekaClientConfig config) { + this(LogFactory.getLog(EurekaConfigServerInstanceProvider.class), client, config); + } + + public EurekaConfigServerInstanceProvider(Log log, EurekaHttpClient client, EurekaClientConfig config) { + this.log = log; + this.client = client; + this.config = config; + } + + public List getInstances(String serviceId) { + if (log.isDebugEnabled()) { + log.debug("eurekaConfigServerInstanceProvider finding instances for " + serviceId); + } + EurekaHttpResponse response = client.getApplications(config.getRegion()); + List instances = new ArrayList<>(); + if (!isSuccessful(response) || response.getEntity() == null) { + return instances; + } + + Applications applications = response.getEntity(); + applications.shuffleInstances(config.shouldFilterOnlyUpInstances()); + List infos = applications.getInstancesByVirtualHostName(serviceId); + for (InstanceInfo info : infos) { + instances.add(new EurekaServiceInstance(info)); + } + if (log.isDebugEnabled()) { + log.debug("eurekaConfigServerInstanceProvider found " + infos.size() + " instance(s) for " + serviceId + + ", " + instances); + } + return instances; + } + + private boolean isSuccessful(EurekaHttpResponse response) { + HttpStatus httpStatus = HttpStatus.resolve(response.getStatusCode()); + return httpStatus != null && httpStatus.is2xxSuccessful(); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/HostnameBasedUrlRandomizer.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/HostnameBasedUrlRandomizer.java new file mode 100644 index 000000000..0a68c99c7 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/config/HostnameBasedUrlRandomizer.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013-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.netflix.eureka.config; + +import java.util.List; + +import com.netflix.discovery.endpoint.EndpointUtils; +import com.netflix.discovery.shared.resolver.DefaultEndpoint; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +final class HostnameBasedUrlRandomizer implements EndpointUtils.ServiceUrlRandomizer { + + private final String hostname; + + HostnameBasedUrlRandomizer(String hostname) { + this.hostname = hostname; + } + + @Override + public void randomize(List urlList) { + int listSize = 0; + if (urlList != null) { + listSize = urlList.size(); + } + if (!StringUtils.hasText(hostname) || listSize == 0) { + return; + } + // Find the hashcode of the instance hostname and use it to find an entry + // and then arrange the rest of the entries after this entry. + int instanceHashcode = hostname.hashCode(); + if (instanceHashcode < 0) { + instanceHashcode = instanceHashcode * -1; + } + int backupInstance = instanceHashcode % listSize; + for (int i = 0; i < backupInstance; i++) { + String zone = urlList.remove(0); + urlList.add(zone); + } + } + + static String getEurekaUrl(EurekaClientConfigBean config, String hostname) { + List urls = EndpointUtils.getDiscoveryServiceUrls(config, EurekaClientConfigBean.DEFAULT_ZONE, + new HostnameBasedUrlRandomizer(hostname)); + return urls.get(0); + } + + static DefaultEndpoint randomEndpoint(EurekaClientConfigBean config, Environment env) { + String hostname = env.getProperty("eureka.instance.hostname"); + return new DefaultEndpoint(getEurekaUrl(config, hostname)); + } + + static DefaultEndpoint randomEndpoint(EurekaClientConfigBean config, Binder binder) { + String hostname = binder.bind("eureka.instance.hostname", String.class).orElseGet(() -> null); + return new DefaultEndpoint(getEurekaUrl(config, hostname)); + } + +} diff --git a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories index 3eeea8fa1..5abd9d067 100644 --- a/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-netflix-eureka-client/src/main/resources/META-INF/spring.factories @@ -8,3 +8,6 @@ org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConf org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapConfiguration + +org.springframework.boot.Bootstrapper=\ +org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapper diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapperTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapperTests.java new file mode 100644 index 000000000..4c6970885 --- /dev/null +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/config/EurekaConfigServerBootstrapperTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-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.netflix.eureka.config; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.config.client.ConfigServerInstanceProvider; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EurekaConfigServerBootstrapperTests { + + @Test + public void notEnabledDoesNotAddInstanceProviderFn() { + new SpringApplicationBuilder(TestConfig.class) + .properties("spring.cloud.service-registry.auto-registration.enabled=false") + .addBootstrapper(registry -> registry.addCloseListener(event -> { + ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() + .get(ConfigServerInstanceProvider.Function.class); + assertThat(providerFn).as("ConfigServerInstanceProvider.Function was created when it shouldn't") + .isNull(); + })).run().close(); + } + + @Test + public void enabledAddsInstanceProviderFn() { + new SpringApplicationBuilder(TestConfig.class) + .properties("spring.cloud.config.discovery.enabled=true", + "spring.cloud.service-registry.auto-registration.enabled=false") + .addBootstrapper(registry -> registry.addCloseListener(event -> { + ConfigServerInstanceProvider.Function providerFn = event.getBootstrapContext() + .get(ConfigServerInstanceProvider.Function.class); + assertThat(providerFn).as("ConfigServerInstanceProvider.Function was not created when it should.") + .isNotNull(); + })).run().close(); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + +}