From 3ebd51cdaa45afa936329322af78110197fe4dbc Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 24 Jul 2020 15:27:41 +0200 Subject: [PATCH] Added support for ReactiveDiscoveryClient; fixes gh-1392 --- .../cloud/StubRunnerDiscoveryClient.java | 14 +- .../StubRunnerReactiveDiscoveryClient.java | 125 ++++++++++++++++++ ...tubRunnerSpringCloudAutoConfiguration.java | 31 +++-- ...activeNoRibbonAutoConfigurationSpec.groovy | 99 ++++++++++++++ ...plication-cloudtest-reactive-no-ribbon.yml | 17 +++ 5 files changed, 262 insertions(+), 24 deletions(-) create mode 100644 spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerReactiveDiscoveryClient.java create mode 100644 spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudReactiveNoRibbonAutoConfigurationSpec.groovy create mode 100644 spring-cloud-contract-stub-runner/src/test/resources/application-cloudtest-reactive-no-ribbon.yml diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java index d0c666486a..458340ab8a 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerDiscoveryClient.java @@ -16,7 +16,6 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud; -import java.lang.invoke.MethodHandles; import java.net.URI; import java.net.URL; import java.util.ArrayList; @@ -41,8 +40,7 @@ import org.springframework.util.StringUtils; */ class StubRunnerDiscoveryClient implements DiscoveryClient { - private static final Log log = LogFactory - .getLog(MethodHandles.lookup().lookupClass()); + private static final Log log = LogFactory.getLog(StubRunnerDiscoveryClient.class); private final DiscoveryClient delegate; @@ -51,7 +49,7 @@ class StubRunnerDiscoveryClient implements DiscoveryClient { private final StubMapperProperties stubMapperProperties; StubRunnerDiscoveryClient(DiscoveryClient delegate, StubFinder stubFinder, - StubMapperProperties stubMapperProperties, String springAppName) { + StubMapperProperties stubMapperProperties) { this.delegate = delegate instanceof StubRunnerDiscoveryClient ? noOpDiscoveryClient() : delegate; if (log.isDebugEnabled()) { @@ -63,7 +61,7 @@ class StubRunnerDiscoveryClient implements DiscoveryClient { } StubRunnerDiscoveryClient(StubFinder stubFinder, - StubMapperProperties stubMapperProperties, String springAppName) { + StubMapperProperties stubMapperProperties) { this.delegate = noOpDiscoveryClient(); if (log.isDebugEnabled()) { log.debug("Will delegate calls to discovery service [" + this.delegate @@ -97,13 +95,13 @@ class StubRunnerDiscoveryClient implements DiscoveryClient { String serviceToFind = StringUtils.hasText(ivyNotation) ? ivyNotation : serviceId; URL stubUrl = this.stubFinder.findStubUrl(serviceToFind); log.info("Resolved from ivy [" + ivyNotation + "] service to find [" - + serviceToFind + "]. " + "Found stub is available under URL [" + stubUrl + + serviceToFind + "]. Found stub is available under URL [" + stubUrl + "]"); if (stubUrl == null) { return getInstancesFromDelegate(serviceId); } - return Collections.singletonList(new StubRunnerServiceInstance( - serviceId, stubUrl.getHost(), stubUrl.getPort(), toUri(stubUrl))); + return Collections.singletonList(new StubRunnerServiceInstance(serviceId, + stubUrl.getHost(), stubUrl.getPort(), toUri(stubUrl))); } private List getInstancesFromDelegate(String serviceId) { diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerReactiveDiscoveryClient.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerReactiveDiscoveryClient.java new file mode 100644 index 0000000000..bc0326e83d --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerReactiveDiscoveryClient.java @@ -0,0 +1,125 @@ +/* + * 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.contract.stubrunner.spring.cloud; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.contract.stubrunner.StubFinder; + +/** + * Custom version of {@link DiscoveryClient} that tries to find an instance in one of the + * started WireMock servers. + * + * @author Marcin Grzejszczak + * @since 2.2.4 + */ +class StubRunnerReactiveDiscoveryClient implements ReactiveDiscoveryClient { + + private static final Log log = LogFactory + .getLog(StubRunnerReactiveDiscoveryClient.class); + + private final ReactiveDiscoveryClient delegate; + + private final StubFinder stubFinder; + + private final StubMapperProperties stubMapperProperties; + + StubRunnerReactiveDiscoveryClient(ReactiveDiscoveryClient delegate, + StubFinder stubFinder, StubMapperProperties stubMapperProperties) { + this.delegate = delegate instanceof StubRunnerDiscoveryClient + ? noOpDiscoveryClient() : delegate; + if (log.isDebugEnabled()) { + log.debug("Will delegate calls to discovery service [" + this.delegate + + "] if a stub is not found"); + } + this.stubFinder = stubFinder; + this.stubMapperProperties = stubMapperProperties; + } + + StubRunnerReactiveDiscoveryClient(StubFinder stubFinder, + StubMapperProperties stubMapperProperties) { + this.delegate = noOpDiscoveryClient(); + if (log.isDebugEnabled()) { + log.debug("Will delegate calls to discovery service [" + this.delegate + + "] if a stub is not found"); + } + this.stubFinder = stubFinder; + this.stubMapperProperties = stubMapperProperties; + } + + private StubRunnerNoOpReactiveDiscoveryClient noOpDiscoveryClient() { + return new StubRunnerNoOpReactiveDiscoveryClient(); + } + + @Override + public String description() { + try { + return this.delegate.description(); + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Failed to fetch description from delegate", e); + } + } + return ""; + } + + @Override + public Flux getInstances(String serviceId) { + return Flux.just(client()).flatMapIterable(c -> c.getInstances(serviceId)); + } + + private StubRunnerDiscoveryClient client() { + return new StubRunnerDiscoveryClient(this.stubFinder, this.stubMapperProperties); + } + + @Override + public Flux getServices() { + return Flux.just(client()) + .flatMapIterable(StubRunnerDiscoveryClient::getServices); + } + + @Override + public int getOrder() { + return this.delegate.getOrder(); + } + +} + +class StubRunnerNoOpReactiveDiscoveryClient implements ReactiveDiscoveryClient { + + @Override + public String description() { + return "Spring Cloud Stub Runner No-op ReactiveDiscoveryClient"; + } + + @Override + public Flux getInstances(String serviceId) { + return Flux.empty(); + } + + @Override + public Flux getServices() { + return Flux.empty(); + } + +} diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java index 3e52e5254f..afacd1a12f 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudAutoConfiguration.java @@ -18,13 +18,13 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; import org.springframework.cloud.contract.stubrunner.StubFinder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,10 +55,18 @@ public class StubRunnerSpringCloudAutoConfiguration { @ConditionalOnProperty(value = "stubrunner.cloud.delegate.enabled", havingValue = "false", matchIfMissing = true) public DiscoveryClient noOpStubRunnerDiscoveryClient(StubFinder stubFinder, - StubMapperProperties stubMapperProperties, - @Value("${spring.application.name:unknown}") String springAppName) { - return new StubRunnerDiscoveryClient(stubFinder, stubMapperProperties, - springAppName); + StubMapperProperties stubMapperProperties) { + return new StubRunnerDiscoveryClient(stubFinder, stubMapperProperties); + } + + @Bean + @ConditionalOnMissingBean(ReactiveDiscoveryClient.class) + @ConditionalOnStubbedDiscoveryEnabled + @ConditionalOnProperty(value = "stubrunner.cloud.delegate.enabled", + havingValue = "false", matchIfMissing = true) + public ReactiveDiscoveryClient noOpStubRunnerReactiveDiscoveryClient( + StubFinder stubFinder, StubMapperProperties stubMapperProperties) { + return new StubRunnerReactiveDiscoveryClient(stubFinder, stubMapperProperties); } } @@ -97,10 +105,9 @@ class StubRunnerDiscoveryClientWrapper implements BeanPostProcessor { } if (isCloudDelegateEnabled()) { return new StubRunnerDiscoveryClient((DiscoveryClient) bean, stubFinder(), - stubMapperProperties(), springAppName()); + stubMapperProperties()); } - return new StubRunnerDiscoveryClient(stubFinder(), stubMapperProperties(), - springAppName()); + return new StubRunnerDiscoveryClient(stubFinder(), stubMapperProperties()); } return bean; } @@ -120,14 +127,6 @@ class StubRunnerDiscoveryClientWrapper implements BeanPostProcessor { return this.stubMapperProperties; } - String springAppName() { - if (this.springAppName == null) { - this.springAppName = this.beanFactory.getBean(Environment.class) - .getProperty("spring.application.name", "unknown"); - } - return this.springAppName; - } - boolean isStubbedDiscoveryEnabled() { if (this.stubbedDiscoveryEnabled == null) { this.stubbedDiscoveryEnabled = Boolean diff --git a/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudReactiveNoRibbonAutoConfigurationSpec.groovy b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudReactiveNoRibbonAutoConfigurationSpec.groovy new file mode 100644 index 0000000000..18b0f2bf61 --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/groovy/org/springframework/cloud/contract/stubrunner/spring/cloud/StubRunnerSpringCloudReactiveNoRibbonAutoConfigurationSpec.groovy @@ -0,0 +1,99 @@ +/* + * 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.contract.stubrunner.spring.cloud + +import org.junit.AfterClass +import org.junit.BeforeClass +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.SpringBootContextLoader +import org.springframework.cloud.client.ServiceInstance +import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient +import org.springframework.cloud.client.loadbalancer.LoadBalanced +import org.springframework.cloud.consul.ConsulAutoConfiguration +import org.springframework.cloud.contract.stubrunner.StubFinder +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner +import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties +import org.springframework.cloud.contract.stubrunner.spring.cloud.loadbalancer.StubRunnerLoadBalancerClientFactory +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory +import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration +import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.web.client.RestTemplate +/** + * @author Marcin Grzejszczak + */ +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@ActiveProfiles("cloudtest-no-ribbon") +// tag::autoconfigure[] +@AutoConfigureStubRunner( + ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance", + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer", + "org.springframework.cloud.contract.verifier.stubs:bootService"], + stubsMode = StubRunnerProperties.StubsMode.REMOTE, + repositoryRoot = "classpath:m2repo/repository/") +// end::autoconfigure[] +class StubRunnerSpringCloudReactiveNoRibbonAutoConfigurationSpec extends Specification { + @Autowired + StubFinder stubFinder + @Autowired + ReactiveDiscoveryClient reactiveDiscoveryClient; + @Autowired + LoadBalancerClientFactory loadBalancerClientFactory; + RestTemplate restTemplate = new RestTemplate() + + @BeforeClass + @AfterClass + static void setupProps() { + System.clearProperty("stubrunner.repository.root") + System.clearProperty("stubrunner.classifier") + } + + def setup() { + assert loadBalancerClientFactory instanceof StubRunnerLoadBalancerClientFactory + } + + // tag::test[] + def 'should make service discovery work'() { + expect: 'WireMocks are running' + "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + and: 'Stubs can be reached via load service discovery' + ServiceInstance loanIssuance = reactiveDiscoveryClient.getInstances('loanIssuance').blockFirst() + restTemplate.getForObject(loanIssuance.uri.toString() + '/name', String) == 'loanIssuance' + ServiceInstance fraudDetection = reactiveDiscoveryClient.getInstances('someNameThatShouldMapFraudDetectionServer').blockFirst() + restTemplate.getForObject(fraudDetection.uri.toString() + '/name', String)== 'fraudDetectionServer' + } + // end::test[] + + @Configuration + @EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration, + ConsulAutoConfiguration, ZookeeperAutoConfiguration]) + static class Config { + + @Bean + @LoadBalanced + RestTemplate restTemplate() { + return new RestTemplate() + } + } +} diff --git a/spring-cloud-contract-stub-runner/src/test/resources/application-cloudtest-reactive-no-ribbon.yml b/spring-cloud-contract-stub-runner/src/test/resources/application-cloudtest-reactive-no-ribbon.yml new file mode 100644 index 0000000000..ab511e383a --- /dev/null +++ b/spring-cloud-contract-stub-runner/src/test/resources/application-cloudtest-reactive-no-ribbon.yml @@ -0,0 +1,17 @@ +stubrunner: + cloud.stubbed.discovery.enabled: true +spring.cloud: + zookeeper: + enabled: false + discovery.enabled: false + consul: + enabled: false + discovery.enabled: false + service-registry.enabled: false +eureka.client.enabled: false +ribbon.eureka.enabled: false +spring: + cloud: + loadbalancer: + ribbon: + enabled: false # Use Reactive Load Balancer