Updates WebClientEurekaHttpClient to use auto configured WebClient.

This allows it to take advantage of settings that are auto-configured such as spring.codec.*.

Fixes gh-3819
This commit is contained in:
spencergibb
2020-07-02 17:40:09 -04:00
parent f6120d525f
commit 8d5a3f8ec1
10 changed files with 200 additions and 60 deletions

View File

@@ -20,6 +20,7 @@ import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
@@ -30,6 +31,7 @@ import org.springframework.cloud.netflix.eureka.http.RestTemplateDiscoveryClient
import org.springframework.cloud.netflix.eureka.http.WebClientDiscoveryClientOptionalArgs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
@@ -37,7 +39,8 @@ import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class DiscoveryClientOptionalArgsConfiguration {
protected final Log logger = LogFactory.getLog(getClass());
protected static final Log logger = LogFactory
.getLog(DiscoveryClientOptionalArgsConfiguration.class);
@Bean
@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
@@ -50,21 +53,6 @@ public class DiscoveryClientOptionalArgsConfiguration {
return new RestTemplateDiscoveryClientOptionalArgs();
}
@Bean
@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnClass(
name = "org.springframework.web.reactive.function.client.WebClient")
@ConditionalOnMissingBean(
value = { AbstractDiscoveryClientOptionalArgs.class,
RestTemplateDiscoveryClientOptionalArgs.class },
search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
havingValue = "true")
public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs() {
logger.info("Eureka HTTP Client uses WebClient.");
return new WebClientDiscoveryClientOptionalArgs();
}
@Bean
@ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class,
@@ -73,6 +61,26 @@ public class DiscoveryClientOptionalArgsConfiguration {
return new MutableDiscoveryClientOptionalArgs();
}
@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnClass(
name = "org.springframework.web.reactive.function.client.WebClient")
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
havingValue = "true")
protected static class WebClientConfiguration {
@Bean
@ConditionalOnMissingBean(
value = { AbstractDiscoveryClientOptionalArgs.class,
RestTemplateDiscoveryClientOptionalArgs.class },
search = SearchStrategy.CURRENT)
public WebClientDiscoveryClientOptionalArgs webClientDiscoveryClientOptionalArgs(
ObjectProvider<WebClient.Builder> builder) {
logger.info("Eureka HTTP Client uses WebClient.");
return new WebClientDiscoveryClientOptionalArgs(builder::getIfAvailable);
}
}
@Configuration
@ConditionalOnMissingClass({ "com.sun.jersey.api.client.filter.ClientFilter",
"org.springframework.web.reactive.function.client.WebClient" })

View File

@@ -29,10 +29,14 @@ 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;
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.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;
@@ -48,6 +52,7 @@ 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;
/**
* Bootstrap configuration for config client that wants to lookup the config server via
@@ -72,18 +77,6 @@ public class EurekaConfigServerBootstrapConfiguration {
return new EurekaClientConfigBean();
}
@Bean
@ConditionalOnMissingBean(EurekaHttpClient.class)
@ConditionalOnClass(
name = "org.springframework.web.reactive.function.client.WebClient")
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
havingValue = "true")
public WebClientEurekaHttpClient configDiscoveryWebClientEurekaHttpClient(
EurekaClientConfigBean config, Environment env) {
return (WebClientEurekaHttpClient) new WebClientTransportClientFactory()
.newClient(new DefaultEndpoint(getEurekaUrl(config, env)));
}
@Bean
@ConditionalOnMissingBean(EurekaHttpClient.class)
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
@@ -94,7 +87,7 @@ public class EurekaConfigServerBootstrapConfiguration {
.newClient(new DefaultEndpoint(getEurekaUrl(config, env)));
}
private String getEurekaUrl(EurekaClientConfigBean config, Environment env) {
private static String getEurekaUrl(EurekaClientConfigBean config, Environment env) {
List<String> urls = EndpointUtils.getDiscoveryServiceUrls(config,
EurekaClientConfigBean.DEFAULT_ZONE, new HostnameBasedUrlRandomizer(
env.getProperty("eureka.instance.hostname")));
@@ -170,4 +163,25 @@ public class EurekaConfigServerBootstrapConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(
name = "org.springframework.web.reactive.function.client.WebClient")
@ConditionalOnProperty(prefix = "eureka.client", name = "webclient.enabled",
havingValue = "true")
@ImportAutoConfiguration({ CodecsAutoConfiguration.class,
WebClientAutoConfiguration.class })
protected static class WebClientConfiguration {
@Bean
@ConditionalOnMissingBean(EurekaHttpClient.class)
public WebClientEurekaHttpClient configDiscoveryWebClientEurekaHttpClient(
EurekaClientConfigBean config, ObjectProvider<WebClient.Builder> builder,
Environment env) {
return (WebClientEurekaHttpClient) new WebClientTransportClientFactory(
builder::getIfAvailable)
.newClient(new DefaultEndpoint(getEurekaUrl(config, env)));
}
}
}

View File

@@ -16,8 +16,12 @@
package org.springframework.cloud.netflix.eureka.http;
import java.util.function.Supplier;
import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
* @author Haytham Mohamed
@@ -25,8 +29,13 @@ import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
public class WebClientDiscoveryClientOptionalArgs
extends AbstractDiscoveryClientOptionalArgs<Void> {
@Deprecated
public WebClientDiscoveryClientOptionalArgs() {
setTransportClientFactories(new WebClientTransportClientFactories());
this(WebClient::builder);
}
public WebClientDiscoveryClientOptionalArgs(Supplier<WebClient.Builder> builder) {
setTransportClientFactories(new WebClientTransportClientFactories(builder));
}
}

View File

@@ -209,6 +209,10 @@ public class WebClientEurekaHttpClient implements EurekaHttpClient {
// Nothing to do
}
public WebClient getWebClient() {
return this.webClient;
}
private static Map<String, String> headersOf(ClientResponse response) {
ClientResponse.Headers httpHeaders = response.headers();
if (httpHeaders == null) {

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.netflix.eureka.http;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Supplier;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
@@ -28,12 +29,20 @@ import com.netflix.discovery.shared.transport.TransportClientFactory;
import com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient;
import com.netflix.discovery.shared.transport.jersey.TransportClientFactories;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
* @author Haytham Mohamed
*/
public class WebClientTransportClientFactories implements TransportClientFactories<Void> {
private final Supplier<WebClient.Builder> builder;
public WebClientTransportClientFactories(Supplier<WebClient.Builder> builder) {
this.builder = builder;
}
@Override
public TransportClientFactory newTransportClientFactory(
Collection<Void> additionalFilters, EurekaJerseyClient providedJerseyClient) {
@@ -44,7 +53,7 @@ public class WebClientTransportClientFactories implements TransportClientFactori
public TransportClientFactory newTransportClientFactory(
EurekaClientConfig clientConfig, Collection<Void> additionalFilters,
InstanceInfo myInstanceInfo) {
return new WebClientTransportClientFactory();
return new WebClientTransportClientFactory(builder);
}
@Override
@@ -53,7 +62,7 @@ public class WebClientTransportClientFactories implements TransportClientFactori
final Collection<Void> additionalFilters, final InstanceInfo myInstanceInfo,
final Optional<SSLContext> sslContext,
final Optional<HostnameVerifier> hostnameVerifier) {
return new WebClientTransportClientFactory();
return new WebClientTransportClientFactory(builder);
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.netflix.eureka.http;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.function.Supplier;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -41,13 +42,13 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
/**
@@ -60,17 +61,29 @@ import org.springframework.web.reactive.function.client.WebClient;
*/
public class WebClientTransportClientFactory implements TransportClientFactory {
private final Supplier<WebClient.Builder> builderSupplier;
@Deprecated
public WebClientTransportClientFactory() {
this(WebClient::builder);
}
public WebClientTransportClientFactory(Supplier<WebClient.Builder> builderSupplier) {
this.builderSupplier = builderSupplier;
}
@Override
public EurekaHttpClient newClient(EurekaEndpoint serviceUrl) {
WebClient.Builder builder = of(serviceUrl.getServiceUrl());
this.setExchangeStrategies(builder);
this.skipHttp400Error(builder);
public EurekaHttpClient newClient(EurekaEndpoint endpoint) {
// we want a copy to modify. Don't change the original
WebClient.Builder builder = this.builderSupplier.get().clone();
setUrl(builder, endpoint.getServiceUrl());
setCodecs(builder);
builder.filter(http4XxErrorExchangeFilterFunction());
return new WebClientEurekaHttpClient(builder.build());
}
private WebClient.Builder of(String serviceUrl) {
private WebClient.Builder setUrl(WebClient.Builder builder, String serviceUrl) {
String url = serviceUrl;
WebClient.Builder builder = WebClient.builder();
try {
URI serviceURI = new URI(serviceUrl);
if (serviceURI.getUserInfo() != null) {
@@ -88,23 +101,17 @@ public class WebClientTransportClientFactory implements TransportClientFactory {
return builder.baseUrl(url);
}
private void setExchangeStrategies(WebClient.Builder builder) {
private void setCodecs(WebClient.Builder builder) {
ObjectMapper objectMapper = objectMapper();
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(clientDefaultCodecsConfigurer -> {
clientDefaultCodecsConfigurer.defaultCodecs()
.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper,
MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper,
MediaType.APPLICATION_JSON));
builder.codecs(configurer -> {
ClientCodecConfigurer.ClientDefaultCodecs defaults = configurer
.defaultCodecs();
defaults.jackson2JsonEncoder(
new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
defaults.jackson2JsonDecoder(
new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
}).build();
builder.exchangeStrategies(strategies);
}
private void skipHttp400Error(WebClient.Builder builder) {
builder.filter(http4XxErrorExchangeFilterFunction());
});
}
// Skip over 4xx http errors
@@ -114,9 +121,8 @@ public class WebClientTransportClientFactory implements TransportClientFactory {
if (clientResponse.statusCode().value() == 400) {
ClientResponse newResponse = ClientResponse.from(clientResponse)
.statusCode(HttpStatus.OK).build();
newResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
newResponse.body(
(clientHttpResponse, context) -> clientHttpResponse.getBody());
return Mono.just(newResponse);
}
return Mono.just(clientResponse);

View File

@@ -0,0 +1,84 @@
/*
* 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.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.netflix.eureka.http.WebClientEurekaHttpClient;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* @author Spencer Gibb
*/
@SpringBootTest(properties = { "spring.cloud.config.discovery.enabled=true",
"eureka.client.webclient.enabled=true",
"spring.codec.max-in-memory-size=310000" }, webEnvironment = RANDOM_PORT)
public class EurekaConfigServerBootstrapConfigurationWebClientIntegrationTests {
@LocalServerPort
private int port;
@Autowired
private WebClientEurekaHttpClient eurekaHttpClient;
@Test
public void webClientRespectsCodecProperties() {
WebClient webClient = eurekaHttpClient.getWebClient();
ClientResponse response = webClient.get().uri("http://localhost:" + port)
.exchange().block();
assertThat(response).isNotNull();
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.bodyToMono(String.class).block()).startsWith("....")
.hasSize(300000);
}
@SpringBootConfiguration
@EnableAutoConfiguration
@RestController
static class WebClientController extends WebSecurityConfigurerAdapter {
@GetMapping
public String hello() {
StringBuilder s = new StringBuilder();
for (int i = 0; i < 300000; i++) {
s.append(".");
}
return s.toString();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
}
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
@@ -49,7 +50,7 @@ public class WebClientEurekaHttpClientTest extends AbstractEurekaHttpClientTest
@Before
public void setup() {
eurekaHttpClient = new WebClientTransportClientFactory()
eurekaHttpClient = new WebClientTransportClientFactory(WebClient::builder)
.newClient(new DefaultEndpoint(serviceUrl));
EurekaInstanceConfigBean config = new EurekaInstanceConfigBean(inetUtils);

View File

@@ -18,6 +18,8 @@ package org.springframework.cloud.netflix.eureka.http;
import org.junit.Test;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
*/
@@ -25,7 +27,8 @@ public class WebClientTransportClientFactoriesTest {
@Test(expected = UnsupportedOperationException.class)
public void testJerseyIsUnsuported() {
new WebClientTransportClientFactories().newTransportClientFactory(null, null);
new WebClientTransportClientFactories(WebClient::builder)
.newTransportClientFactory(null, null);
}
}

View File

@@ -21,6 +21,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author Daniel Lavoie
*/
@@ -30,7 +32,7 @@ public class WebClientTransportClientFactoryTest {
@Before
public void setup() {
transportClientFatory = new WebClientTransportClientFactory();
transportClientFatory = new WebClientTransportClientFactory(WebClient::builder);
}
@Test