Ability to supply custom reactive ClientHttpConnector.

We now wrap ClientHttpConnector with a wrapper (ClientHttpConnectorWrapper) to allow supplying a custom ClientHttpConnector.

Closes gh-564.
This commit is contained in:
Mark Paluch
2021-03-11 11:40:59 +01:00
parent 60ed7b7ea3
commit 8fcda41339
3 changed files with 134 additions and 31 deletions

View File

@@ -40,12 +40,12 @@ import org.springframework.boot.context.config.ConfigDataLoaderContext;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.cloud.vault.config.VaultAutoConfiguration.TaskSchedulerWrapper;
import org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.PropertySource;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ClassUtils;
@@ -170,7 +170,7 @@ public class VaultConfigDataLoader implements ConfigDataLoader<VaultConfigLocati
private void registerImperativeInfrastructure(ConfigurableBootstrapContext bootstrap,
VaultProperties vaultProperties) {
ImperativeInfrastructure infra = new ImperativeInfrastructure(bootstrap, vaultProperties, logFactory);
ImperativeInfrastructure infra = new ImperativeInfrastructure(bootstrap, vaultProperties, this.logFactory);
infra.registerClientHttpRequestFactoryWrapper();
infra.registerRestTemplateBuilder();
@@ -200,8 +200,8 @@ public class VaultConfigDataLoader implements ConfigDataLoader<VaultConfigLocati
VaultProperties vaultProperties) {
ReactiveInfrastructure reactiveInfrastructure = new ReactiveInfrastructure(bootstrap, vaultProperties,
logFactory);
reactiveInfrastructure.registerClientHttpConnector();
this.logFactory);
reactiveInfrastructure.registerClientHttpConnectorWrapper();
reactiveInfrastructure.registerWebClientBuilder();
reactiveInfrastructure.registerWebClientFactory();
@@ -522,22 +522,22 @@ public class VaultConfigDataLoader implements ConfigDataLoader<VaultConfigLocati
this.logFactory = logFactory;
}
void registerClientHttpConnector() {
// not a bean
this.bootstrap.registerIfAbsent(ClientHttpConnector.class,
ctx -> this.configuration.createClientHttpConnector());
void registerClientHttpConnectorWrapper() {
registerIfAbsent(this.bootstrap, "clientHttpConnectorWrapper", ClientHttpConnectorWrapper.class,
() -> new ClientHttpConnectorWrapper(this.configuration.createClientHttpConnector()));
}
public void registerWebClientBuilder() {
// not a bean
this.bootstrap.registerIfAbsent(WebClientBuilder.class,
ctx -> this.configuration.createWebClientBuilder(ctx.get(ClientHttpConnector.class),
this.endpointProvider, Collections.emptyList()));
ctx -> this.configuration.createWebClientBuilder(
ctx.get(ClientHttpConnectorWrapper.class).getConnector(), this.endpointProvider,
Collections.emptyList()));
}
void registerWebClientFactory() {
registerIfAbsent(this.bootstrap, "vaultWebClientFactory", WebClientFactory.class,
ctx -> new DefaultWebClientFactory(ctx.get(ClientHttpConnector.class),
ctx -> new DefaultWebClientFactory(ctx.get(ClientHttpConnectorWrapper.class).getConnector(),
connector -> this.configuration.createWebClientBuilder(connector, this.endpointProvider,
Collections.emptyList())));
}

View File

@@ -24,10 +24,12 @@ import reactor.core.publisher.Flux;
import reactor.netty.http.client.HttpClient;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -35,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@@ -79,6 +82,9 @@ import static org.springframework.cloud.vault.config.VaultAutoConfiguration.Task
@AutoConfigureBefore(VaultAutoConfiguration.class)
public class VaultReactiveAutoConfiguration implements InitializingBean {
@Nullable
private final ConfigurableApplicationContext applicationContext;
private final VaultProperties vaultProperties;
private final VaultReactiveConfiguration configuration;
@@ -91,20 +97,26 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
private final List<WebClientCustomizer> customizers;
@Nullable
private ClientHttpConnector clientHttpConnector;
/**
* Used for Vault communication.
* @deprecated since 3.0.2, use
* {@link #VaultReactiveAutoConfiguration(ConfigurableApplicationContext, VaultProperties, ObjectProvider, ObjectProvider, ObjectProvider)}
* instead.
*/
@Nullable
private WebClientBuilder webClientBuilder;
@Deprecated
public VaultReactiveAutoConfiguration(VaultProperties vaultProperties,
ObjectProvider<ReactiveVaultEndpointProvider> reactiveEndpointProvider,
ObjectProvider<VaultEndpointProvider> endpointProvider,
ObjectProvider<List<WebClientCustomizer>> webClientCustomizers) {
this(null, vaultProperties, reactiveEndpointProvider, endpointProvider, webClientCustomizers);
}
@Autowired
public VaultReactiveAutoConfiguration(@Nullable ConfigurableApplicationContext applicationContext,
VaultProperties vaultProperties, ObjectProvider<ReactiveVaultEndpointProvider> reactiveEndpointProvider,
ObjectProvider<VaultEndpointProvider> endpointProvider,
ObjectProvider<List<WebClientCustomizer>> webClientCustomizers) {
this.applicationContext = applicationContext;
this.vaultProperties = vaultProperties;
this.configuration = new VaultReactiveConfiguration(vaultProperties);
@@ -122,14 +134,6 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
AnnotationAwareOrderComparator.sort(this.customizers);
}
@Override
public void afterPropertiesSet() {
this.clientHttpConnector = createConnector(this.vaultProperties);
this.webClientBuilder = webClientBuilder(this.clientHttpConnector);
}
protected WebClientBuilder webClientBuilder(ClientHttpConnector connector) {
if (this.reactiveEndpointProvider != null) {
@@ -145,6 +149,26 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
"Cannot create WebClientBuilder as neither ReactiveEndpointProvider nor EndpointProvider configured");
}
/**
* Creates a {@link ClientHttpConnectorWrapper} containing a
* {@link ClientHttpConnector}. {@link ClientHttpConnector} is not exposed as root
* bean because {@link ClientHttpConnector} is configured with {@link ClientOptions}
* and {@link SslConfiguration} which are not necessarily applicable for the whole
* application.
* @return the {@link ClientHttpConnectorWrapper} to wrap a
* {@link ClientHttpConnector} instance.
* @since 3.0.2
*/
@Bean
@ConditionalOnMissingBean
public ClientHttpConnectorWrapper clientHttpConnectorWrapper() {
return new ClientHttpConnectorWrapper(createConnector(this.vaultProperties));
}
@Override
public void afterPropertiesSet() {
}
/**
* Creates a {@link ClientHttpConnector} configured with {@link ClientOptions} and
* {@link SslConfiguration} which are not necessarily applicable for the whole
@@ -165,9 +189,18 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
@ConditionalOnMissingBean
public WebClientFactory vaultWebClientFactory() {
Assert.state(this.clientHttpConnector != null, "ClientHttpConnector must not be null");
ClientHttpConnector connector = getClientHttpConnector();
return new DefaultWebClientFactory(this.clientHttpConnector, this::webClientBuilder);
return new DefaultWebClientFactory(connector, this::webClientBuilder);
}
protected ClientHttpConnector getClientHttpConnector() {
if (this.applicationContext != null) {
return this.applicationContext.getBean(ClientHttpConnectorWrapper.class).getConnector();
}
return createConnector(this.vaultProperties);
}
/**
@@ -180,13 +213,13 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
@ConditionalOnMissingBean(ReactiveVaultOperations.class)
public ReactiveVaultTemplate reactiveVaultTemplate(ObjectProvider<ReactiveSessionManager> sessionManager) {
Assert.state(this.webClientBuilder != null, "WebClientBuilder must not be null");
WebClientBuilder webClientBuilder = webClientBuilder(getClientHttpConnector());
if (this.vaultProperties.getAuthentication() == VaultProperties.AuthenticationMethod.NONE) {
return new ReactiveVaultTemplate(this.webClientBuilder);
return new ReactiveVaultTemplate(webClientBuilder);
}
return new ReactiveVaultTemplate(this.webClientBuilder, sessionManager.getObject());
return new ReactiveVaultTemplate(webClientBuilder, sessionManager.getObject());
}
/**
@@ -241,4 +274,38 @@ public class VaultReactiveAutoConfiguration implements InitializingBean {
() -> beanFactory.getBeanProvider(ClientAuthentication.class, false).getIfAvailable());
}
/**
* Wrapper for {@link ClientHttpConnector} to not expose the bean globally.
*
* @since 3.0.2
*/
public static class ClientHttpConnectorWrapper implements InitializingBean, DisposableBean {
private final ClientHttpConnector connector;
public ClientHttpConnectorWrapper(ClientHttpConnector connector) {
this.connector = connector;
}
@Override
public void destroy() throws Exception {
if (this.connector instanceof DisposableBean) {
((DisposableBean) this.connector).destroy();
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.connector instanceof InitializingBean) {
((InitializingBean) this.connector).afterPropertiesSet();
}
}
public ClientHttpConnector getConnector() {
return this.connector;
}
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.vault.authentication.AuthenticationSteps;
@@ -45,8 +46,12 @@ import org.springframework.vault.core.ReactiveVaultOperations;
import org.springframework.vault.core.ReactiveVaultTemplate;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for {@link VaultReactiveAutoConfiguration}.
@@ -184,6 +189,24 @@ public class VaultReactiveAutoConfigurationTests {
});
}
@Test
public void shouldConsiderCustomConnector() {
this.contextRunner
.withPropertyValues("spring.cloud.vault.kv.enabled=false", "spring.cloud.vault.token=foo",
"spring.cloud.vault.session.lifecycle.enabled=false")
.withUserConfiguration(CustomConnector.class)
.withBean("vaultTokenSupplier", VaultTokenSupplier.class,
() -> () -> Mono.just(VaultToken.of("foo".toCharArray())))
.withBean("taskSchedulerWrapper", VaultAutoConfiguration.TaskSchedulerWrapper.class,
() -> new VaultAutoConfiguration.TaskSchedulerWrapper(new ThreadPoolTaskScheduler()))
.run(context -> {
ReactiveVaultOperations operations = context.getBean(ReactiveVaultOperations.class);
operations.delete("foo").as(StepVerifier::create).verifyError(WebClientRequestException.class);
});
}
@Configuration(proxyBeanMethods = false)
static class AuthenticationFactoryConfiguration {
@@ -234,4 +257,17 @@ public class VaultReactiveAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomConnector {
@Bean
VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper myWrapper() {
ClientHttpConnector mock = mock(ClientHttpConnector.class);
when(mock.connect(any(), any(), any())).thenReturn(Mono.error(new UnsupportedOperationException()));
return new VaultReactiveAutoConfiguration.ClientHttpConnectorWrapper(mock);
}
}
}