Introduce WebClientFactory and RestTemplateFactory

We now encapsulate WebClient and RestTemplate creation through WebClientFactory and RestTemplateFactory beans. These interfaces provide safe access through the related builders to HTTP client creation without the risk of using a mutable builder whose state might have changed in the meantime.

Those reusable components reduce the need of code duplication when calling code wants to participate in HTTP clients created by the configuration infrastructure.

Closes gh-554.
This commit is contained in:
Mark Paluch
2020-04-29 15:10:15 +02:00
parent 17ed99a632
commit f607a679e5
10 changed files with 516 additions and 21 deletions

View File

@@ -18,11 +18,13 @@ package org.springframework.vault.client;
import org.springframework.web.client.RestTemplate;
/**
* Callback interface that can be used to customize a {@link RestTemplate}.
* Callback interface that can be used to customize a {@link RestTemplate}. Beans
* implementing this interface are applied to
* {@link org.springframework.vault.client.RestTemplateBuilder}.
*
* @author Mark Paluch
* @since 2.2
* @see RestTemplateBuilder
* @see org.springframework.vault.client.RestTemplateBuilder#customizers(RestTemplateCustomizer...)
*/
@FunctionalInterface
public interface RestTemplateCustomizer {

View File

@@ -0,0 +1,53 @@
/*
* Copyright 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.vault.client;
import java.util.function.Consumer;
import org.springframework.web.client.RestTemplate;
/**
* Factory interface that produces a {@link RestTemplate} object. Factory implementations
* are expected to create a new {@link RestTemplate} object when calling
* {@link #create()}.
*
* @author Mark Paluch
* @since 2.3
* @see RestTemplateBuilder
* @see RestTemplate
*/
@FunctionalInterface
public interface RestTemplateFactory {
/**
* Create a {@link RestTemplate} instance.
*
* @return a {@link RestTemplate} instance.
*/
default RestTemplate create() {
return create(builder -> {
});
}
/**
* Create a {@link RestTemplate} instance by applying {@code customizer} to the
* underlying {@link RestTemplateBuilder}.
*
* @param customizer builder customizer.
* @return a {@link RestTemplate} instance.
*/
RestTemplate create(Consumer<RestTemplateBuilder> customizer);
}

View File

@@ -18,11 +18,13 @@ package org.springframework.vault.client;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Callback interface that can be used to customize a {@link WebClient.Builder}.
* Callback interface that can be used to customize a {@link WebClient.Builder}. Beans
* implementing this interface are applied to
* {@link org.springframework.vault.client.WebClientBuilder}.
*
* @author Mark Paluch
* @since 2.2
* @see WebClientBuilder
* @see org.springframework.vault.client.WebClientBuilder#customizers(WebClientCustomizer...)
*/
@FunctionalInterface
public interface WebClientCustomizer {

View File

@@ -0,0 +1,52 @@
/*
* Copyright 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.vault.client;
import java.util.function.Consumer;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Factory interface that produces a {@link WebClient} object. Factory implementations are
* expected to create a new {@link WebClient} object when calling {@link #create()}.
*
* @author Mark Paluch
* @since 2.3
* @see WebClientBuilder
* @see WebClient
*/
@FunctionalInterface
public interface WebClientFactory {
/**
* Create a {@link WebClient} instance.
*
* @return a {@link WebClient} instance.
*/
default WebClient create() {
return create(builder -> {
});
}
/**
* Create a {@link WebClient} instance by applying {@code customizer} to the
* underlying {@link WebClientBuilder}.
*
* @param customizer builder customizer.
* @return a {@link WebClient} instance.
*/
WebClient create(Consumer<WebClientBuilder> customizer);
}

View File

@@ -19,6 +19,7 @@ import java.time.Duration;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -33,9 +34,11 @@ import org.springframework.vault.authentication.SessionManager;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.authentication.VaultTokenSupplier;
import org.springframework.vault.client.ClientHttpConnectorFactory;
import org.springframework.vault.client.ReactiveVaultClients;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.client.VaultEndpointProvider;
import org.springframework.vault.client.WebClientBuilder;
import org.springframework.vault.client.WebClientCustomizer;
import org.springframework.vault.client.WebClientFactory;
import org.springframework.vault.core.ReactiveVaultTemplate;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.VaultToken;
@@ -75,8 +78,33 @@ public abstract class AbstractReactiveVaultConfiguration
*/
protected WebClientBuilder webClientBuilder(VaultEndpointProvider endpointProvider,
ClientHttpConnector httpConnector) {
return WebClientBuilder.builder().endpointProvider(endpointProvider)
.httpConnector(httpConnector);
ObjectProvider<WebClientCustomizer> customizers = getBeanFactory()
.getBeanProvider(WebClientCustomizer.class);
WebClientBuilder builder = WebClientBuilder.builder()
.endpointProvider(endpointProvider).httpConnector(httpConnector);
builder.customizers(customizers.stream().toArray(WebClientCustomizer[]::new));
return builder;
}
/**
* Create a {@link WebClientFactory} bean that is used to produce a {@link WebClient}.
*
* @return the {@link WebClientFactory}.
* @see #clientHttpConnector()
* @since 2.3
*/
@Bean
public WebClientFactory webClientFactory() {
ClientHttpConnector httpConnector = clientHttpConnector();
return new DefaultWebClientFactory(httpConnector, clientHttpConnector -> {
return webClientBuilder(vaultEndpointProvider(), clientHttpConnector);
});
}
/**
@@ -89,8 +117,12 @@ public abstract class AbstractReactiveVaultConfiguration
*/
@Bean
public ReactiveVaultTemplate reactiveVaultTemplate() {
VaultEndpointProvider provider = vaultEndpointProvider();
VaultEndpoint vaultEndpoint = provider.getVaultEndpoint();
return new ReactiveVaultTemplate(
webClientBuilder(vaultEndpointProvider(), clientHttpConnector()),
webClientBuilder(() -> vaultEndpoint, clientHttpConnector()),
getReactiveSessionManager());
}
@@ -118,8 +150,8 @@ public abstract class AbstractReactiveVaultConfiguration
@Bean
public ReactiveSessionManager reactiveSessionManager() {
WebClient webClient = ReactiveVaultClients.createWebClient(vaultEndpoint(),
clientHttpConnector());
WebClient webClient = getWebClientFactory().create();
return new ReactiveLifecycleAwareSessionManager(vaultTokenSupplier(),
getVaultThreadPoolTaskScheduler(), webClient);
}
@@ -147,8 +179,7 @@ public abstract class AbstractReactiveVaultConfiguration
AuthenticationStepsFactory factory = (AuthenticationStepsFactory) clientAuthentication;
WebClient webClient = ReactiveVaultClients.createWebClient(vaultEndpoint(),
clientHttpConnector());
WebClient webClient = getWebClientFactory().create();
AuthenticationStepsOperator stepsOperator = new AuthenticationStepsOperator(
factory.getAuthenticationSteps(), webClient);
@@ -173,6 +204,16 @@ public abstract class AbstractReactiveVaultConfiguration
return ClientHttpConnectorFactory.create(clientOptions(), sslConfiguration());
}
/**
* Return the {@link WebClientFactory}.
*
* @return the {@link WebClientFactory} bean.
* @since 2.3
*/
protected WebClientFactory getWebClientFactory() {
return getBeanFactory().getBean(WebClientFactory.class);
}
private ReactiveSessionManager getReactiveSessionManager() {
return getBeanFactory().getBean("reactiveSessionManager",
ReactiveSessionManager.class);

View File

@@ -19,6 +19,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
@@ -33,6 +34,8 @@ import org.springframework.vault.authentication.LifecycleAwareSessionManager;
import org.springframework.vault.authentication.SessionManager;
import org.springframework.vault.client.ClientHttpRequestFactoryFactory;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.RestTemplateCustomizer;
import org.springframework.vault.client.RestTemplateFactory;
import org.springframework.vault.client.SimpleVaultEndpointProvider;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.client.VaultEndpointProvider;
@@ -41,6 +44,7 @@ import org.springframework.vault.core.lease.SecretLeaseContainer;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Base class for Spring Vault configuration using JavaConfig.
@@ -84,13 +88,40 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
* @return the {@link RestTemplateBuilder}.
* @see #vaultEndpointProvider()
* @see #clientHttpRequestFactoryWrapper()
* @since 2.2
* @since 2.3
*/
protected RestTemplateBuilder restTemplateBuilder(
VaultEndpointProvider endpointProvider,
ClientHttpRequestFactory requestFactory) {
return RestTemplateBuilder.builder().endpointProvider(endpointProvider)
.requestFactory(requestFactory);
ObjectProvider<RestTemplateCustomizer> customizers = getBeanFactory()
.getBeanProvider(RestTemplateCustomizer.class);
RestTemplateBuilder builder = RestTemplateBuilder.builder()
.endpointProvider(endpointProvider).requestFactory(requestFactory);
builder.customizers(customizers.stream().toArray(RestTemplateCustomizer[]::new));
return builder;
}
/**
* Create a {@link RestTemplateFactory} bean that is used to produce
* {@link RestTemplate}.
*
* @return the {@link RestTemplateFactory}.
* @see #vaultEndpointProvider()
* @see #clientHttpRequestFactoryWrapper()
* @since 2.3
*/
@Bean
public RestTemplateFactory restTemplateFactory(
ClientFactoryWrapper requestFactoryWrapper) {
return new DefaultRestTemplateFactory(
requestFactoryWrapper.getClientHttpRequestFactory(), it -> {
return restTemplateBuilder(vaultEndpointProvider(), it);
});
}
/**
@@ -179,16 +210,14 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
/**
* Construct a {@link RestOperations} object configured for Vault session management
* and authentication usage. Can be customized by overriding
* {@link #restTemplateBuilder(VaultEndpointProvider, ClientHttpRequestFactory)}.
* and authentication usage. Can be customized by providing a
* {@link RestTemplateFactory} bean.
*
* @return the {@link RestOperations} to be used for Vault access.
* @see #vaultEndpointProvider()
* @see #clientHttpRequestFactoryWrapper()
* @see #restTemplateFactory(ClientFactoryWrapper)
*/
public RestOperations restOperations() {
return restTemplateBuilder(vaultEndpointProvider(),
getClientFactoryWrapper().getClientHttpRequestFactory()).build();
return getRestTemplateFactory().create();
}
/**
@@ -249,6 +278,16 @@ public abstract class AbstractVaultConfiguration implements ApplicationContextAw
this.applicationContext = applicationContext;
}
/**
* Return the {@link RestTemplateFactory}.
*
* @return the {@link RestTemplateFactory} bean.
* @since 2.3
*/
protected RestTemplateFactory getRestTemplateFactory() {
return getBeanFactory().getBean(RestTemplateFactory.class);
}
BeanFactory getBeanFactory() {
Assert.state(applicationContext != null,

View File

@@ -0,0 +1,54 @@
/*
* Copyright 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.vault.config;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.lang.Nullable;
import org.springframework.vault.client.RestTemplateBuilder;
import org.springframework.vault.client.RestTemplateFactory;
import org.springframework.web.client.RestTemplate;
/**
* Default {@link RestTemplateFactory} implementation.
*
* @author Mark Paluch
*/
class DefaultRestTemplateFactory implements RestTemplateFactory {
private final ClientHttpRequestFactory requestFactory;
private final Function<ClientHttpRequestFactory, RestTemplateBuilder> builderFunction;
DefaultRestTemplateFactory(ClientHttpRequestFactory requestFactory,
Function<ClientHttpRequestFactory, RestTemplateBuilder> builderFunction) {
this.requestFactory = requestFactory;
this.builderFunction = builderFunction;
}
@Override
public RestTemplate create(@Nullable Consumer<RestTemplateBuilder> customizer) {
RestTemplateBuilder builder = builderFunction.apply(requestFactory);
if (customizer != null) {
customizer.accept(builder);
}
return builder.build();
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 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.vault.config;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.vault.client.WebClientBuilder;
import org.springframework.vault.client.WebClientFactory;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Default implementation of {@link WebClientFactory}.
*
* @author Mark Paluch
* @since 2.3
*/
class DefaultWebClientFactory implements WebClientFactory {
private final ClientHttpConnector connector;
private final Function<ClientHttpConnector, WebClientBuilder> builderFunction;
DefaultWebClientFactory(ClientHttpConnector connector,
Function<ClientHttpConnector, WebClientBuilder> builderFunction) {
this.connector = connector;
this.builderFunction = builderFunction;
}
@Override
public WebClient create(@Nullable Consumer<WebClientBuilder> customizer) {
WebClientBuilder builder = builderFunction.apply(connector);
if (customizer != null) {
customizer.accept(builder);
}
return builder.build();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 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.vault.config;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.client.WebClientCustomizer;
import org.springframework.vault.client.WebClientFactory;
import org.springframework.vault.core.ReactiveVaultOperations;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.vault.util.Settings;
import org.springframework.vault.util.TestRestTemplateFactory;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Integration tests for {@link AbstractReactiveVaultConfiguration}.
*
* @author Mark Paluch
*/
class AbstractReactiveVaultConfigurationUnitTests {
@Test
void shouldApplyCustomizerToWebClientFactory() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WebClientCustomizerConfiguration.class);
WebClientFactory factory = context.getBean(WebClientFactory.class);
WebClient webClient = factory.create();
webClient.get().uri("/foo").exchange().as(StepVerifier::create)
.verifyError(CustomizedSignal.class);
}
@Test
void shouldApplyCustomizerToTemplate() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
WebClientCustomizerConfiguration.class);
ReactiveVaultOperations operations = context
.getBean(ReactiveVaultOperations.class);
operations.read("/foo").as(StepVerifier::create)
.verifyError(CustomizedSignal.class);
}
@Configuration(proxyBeanMethods = false)
static class WebClientCustomizerConfiguration
extends AbstractReactiveVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
return TestRestTemplateFactory.TEST_VAULT_ENDPOINT;
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication(Settings.token());
}
@Override
public SslConfiguration sslConfiguration() {
return Settings.createSslConfiguration();
}
@Bean
public WebClientCustomizer customizer() {
return builder -> builder.exchangeFunction(request -> {
throw new CustomizedSignal();
});
}
}
static class CustomizedSignal extends RuntimeException {
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 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.vault.config;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.RestTemplateCustomizer;
import org.springframework.vault.client.RestTemplateFactory;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.vault.util.Settings;
import org.springframework.vault.util.TestRestTemplateFactory;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Integration tests for {@link AbstractVaultConfiguration}.
*
* @author Mark Paluch
*/
class AbstractVaultConfigurationUnitTests {
@Test
void shouldApplyCustomizerToRestTemplateFactory() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
RestTemplateCustomizerConfiguration.class);
RestTemplateFactory factory = context.getBean(RestTemplateFactory.class);
RestTemplate restTemplate = factory.create();
assertThatExceptionOfType(CustomizedSignal.class)
.isThrownBy(() -> restTemplate.delete("/foo"));
}
@Test
void shouldApplyCustomizerToTemplate() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
RestTemplateCustomizerConfiguration.class);
VaultOperations operations = context.getBean(VaultOperations.class);
assertThatExceptionOfType(CustomizedSignal.class)
.isThrownBy(() -> operations.opsForSys().health());
}
@Configuration(proxyBeanMethods = false)
static class RestTemplateCustomizerConfiguration extends AbstractVaultConfiguration {
@Override
public VaultEndpoint vaultEndpoint() {
return TestRestTemplateFactory.TEST_VAULT_ENDPOINT;
}
@Override
public ClientAuthentication clientAuthentication() {
return new TokenAuthentication(Settings.token());
}
@Override
public SslConfiguration sslConfiguration() {
return Settings.createSslConfiguration();
}
@Bean
public RestTemplateCustomizer customizer() {
return restTemplate -> restTemplate
.setRequestFactory((ClientHttpRequestFactory) (uri, httpMethod) -> {
throw new CustomizedSignal();
});
}
}
static class CustomizedSignal extends RuntimeException {
}
}