Muti binder customization (#2051)

* Muti binder customization

Enable applications with multi binders to provide customizations for
the binders. This is accomplished by providing a new API called
BinderCustomizer. When an implementation is found for this interface,
the customizations are applied against the binders before providing access to them.

Tests and docs.

Resolves https://github.com/spring-cloud/spring-cloud-stream/issues/1541

* Addressing PR review comments

* Addressing PR review
This commit is contained in:
Soby Chacko
2020-11-18 16:32:05 -05:00
committed by GitHub
parent d574ee15be
commit 442c72b37c
9 changed files with 122 additions and 8 deletions

View File

@@ -1627,6 +1627,38 @@ environment:
profiles:
active: myBinderProfile
----
[[binder-customizer]]
=== Customizing binders in multi binder applications
When an application has multiple binders in it and wants to customize the binders, then that can be achieved by providing a `BinderCustomizer` implementation.
In the case of applications with a single binder, this special customizer is not necessary since the binder context can access the customization beans directly.
However, this is not the case in a multi-binder scenario, since various binders live in different application contexts.
By providing an implementation of `BinderCustomizer` interface, the binders, although reside in different application contexts, will receive the customization.
Spring Cloud Stream ensures that the customizations take place before the applications start using the binders.
The user must check for the binder type and then apply the necessary customizations.
Here is an example of providing a `BinderCustomizer` bean.
```
@Bean
public BinderCustomizer binderCustomizer() {
return (binder, binderName) -> {
if (binder instanceof KafkaMessageChannelBinder) {
((KafkaMessageChannelBinder) binder).setRebalanceListener(...);
}
else if (binder instanceof KStreamBinder) {
...
}
else if (binder instanceof RabbitMessageChannelBinder) {
...
}
};
}
```
Note that, when there are more than one instance of the same type of the binder, the binder name can be used to filter customization.
[[binding_visualization_control]]
=== Binding visualization and control
Since version 2.0, Spring Cloud Stream supports visualization and control of the Bindings through Actuator endpoints.

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2015-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.stream.binder;
/**
* @author Soby Chacko
* @since 3.1.0
*/
public interface BinderCustomizer {
/**
* When customization beans are present in an application that uses a single binder,
* those beans are detected by the binder. However, this is not the case in a multi-binder
* scenario, since various binders live in different application contexts. This customizer
* enables the application to properly apply customizations in all the binders.
* By providing an implementation of this interface, the binders, although reside
* in different application contexts, will receive the customization.
* Spring Cloud Stream ensures that the customizations take place before the binders are
* accessed. The user must check for the binder type and then apply the necessary customizations.
* In the case of a single binder, the use of this customizer is redundant.
*
* @param binder to be customized
* @param binderName binder name to distinguish between multiple instances of the same binder type
*/
void customize(Binder<?, ConsumerProperties, ProducerProperties> binder, String binderName);
}

View File

@@ -77,6 +77,8 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
private final BinderTypeRegistry binderTypeRegistry;
private final BinderCustomizer binderCustomizer;
private volatile ConfigurableApplicationContext context;
private Collection<Listener> listeners;
@@ -84,9 +86,10 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
private volatile String defaultBinder;
public DefaultBinderFactory(Map<String, BinderConfiguration> binderConfigurations,
BinderTypeRegistry binderTypeRegistry) {
BinderTypeRegistry binderTypeRegistry, BinderCustomizer binderCustomizer) {
this.binderConfigurations = new HashMap<>(binderConfigurations);
this.binderTypeRegistry = binderTypeRegistry;
this.binderCustomizer = binderCustomizer;
}
@Override
@@ -139,6 +142,9 @@ public class DefaultBinderFactory implements BinderFactory, DisposableBean, Appl
*/
binder = this.doGetBinder(binderName, bindingTargetType);
}
if (this.binderCustomizer != null) {
this.binderCustomizer.customize(binder, binderName);
}
return binder;
}

View File

@@ -23,6 +23,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -32,6 +33,7 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.cloud.stream.binder.BinderConfiguration;
import org.springframework.cloud.stream.binder.BinderCustomizer;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.binder.BinderType;
import org.springframework.cloud.stream.binder.BinderTypeRegistry;
@@ -177,11 +179,12 @@ public class BindingServiceConfiguration {
@Bean
@ConditionalOnMissingBean(BinderFactory.class)
public BinderFactory binderFactory(BinderTypeRegistry binderTypeRegistry,
BindingServiceProperties bindingServiceProperties) {
BindingServiceProperties bindingServiceProperties,
ObjectProvider<BinderCustomizer> binderCustomizerProvider) {
DefaultBinderFactory binderFactory = new DefaultBinderFactory(
getBinderConfigurations(binderTypeRegistry, bindingServiceProperties),
binderTypeRegistry);
binderTypeRegistry, binderCustomizerProvider.getIfUnique());
binderFactory.setDefaultBinder(bindingServiceProperties.getDefaultBinder());
binderFactory.setListeners(this.binderFactoryListeners);
return binderFactory;

View File

@@ -289,8 +289,10 @@ public class BinderFactoryAutoConfigurationTests {
Binder binder1 = binderFactory.getBinder("binder1", MessageChannel.class);
assertThat(binder1).isInstanceOf(StubBinder1.class);
assertThat(((StubBinder1) binder1).getFromCustomization()).isEqualTo("customizer-applied-binder1");
Binder binder2 = binderFactory.getBinder("binder2", MessageChannel.class);
assertThat(binder2).isInstanceOf(StubBinder2.class);
assertThat(((StubBinder2) binder2).getFromCustomization()).isEqualTo("customizer-applied-binder2");
Binder defaultBinder = binderFactory.getBinder(null, MessageChannel.class);
assertThat(defaultBinder).isSameAs(binder2);
@@ -312,6 +314,17 @@ public class BinderFactoryAutoConfigurationTests {
}
@Bean
public BinderCustomizer binderCustomizer() {
return (binder, binderName) -> {
if (binder instanceof StubBinder1) {
((StubBinder1) binder).setFromCustomization("customizer-applied-binder1");
}
else if (binder instanceof StubBinder2) {
((StubBinder2) binder).setFromCustomization("customizer-applied-binder2");
}
};
}
}
@Configuration
@@ -321,7 +334,6 @@ public class BinderFactoryAutoConfigurationTests {
public String fooBean() {
return "foo";
}
}
}

View File

@@ -62,7 +62,7 @@ public class ExtendedPropertiesDefaultTests {
return new DefaultBinderFactory(
Collections.singletonMap("mock",
new BinderConfiguration("mock", new HashMap<>(), true, true)),
binderTypeRegistry);
binderTypeRegistry, null);
}
private DefaultBinderTypeRegistry createMockExtendedBinderTypeRegistry() {

View File

@@ -34,6 +34,8 @@ public class StubBinder1
private ConfigurableApplicationContext outerContext;
private String fromCustomization;
public ConfigurableApplicationContext getOuterContext() {
return this.outerContext;
}
@@ -62,4 +64,11 @@ public class StubBinder1
return null;
}
public String getFromCustomization() {
return fromCustomization;
}
public void setFromCustomization(String fromCustomization) {
this.fromCustomization = fromCustomization;
}
}

View File

@@ -31,6 +31,8 @@ public class StubBinder2
@SuppressWarnings("unused")
private final StubBinder2Dependency stubBinder2Dependency;
private String fromCustomization;
public StubBinder2(StubBinder2Dependency stubBinder2Dependency) {
this.stubBinder2Dependency = stubBinder2Dependency;
}
@@ -47,4 +49,11 @@ public class StubBinder2
return null;
}
public String getFromCustomization() {
return fromCustomization;
}
public void setFromCustomization(String fromCustomization) {
this.fromCustomization = fromCustomization;
}
}

View File

@@ -38,6 +38,7 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
@@ -530,6 +531,7 @@ public class BindingServiceTests {
}
}
@SuppressWarnings("unchecked")
@Test
public void testUnrecognizedBinderAllowedIfNotUsed() {
HashMap<String, String> properties = new HashMap<>();
@@ -541,13 +543,14 @@ public class BindingServiceTests {
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
properties);
BinderFactory binderFactory = new BindingServiceConfiguration()
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties);
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class));
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory);
bindingService.bindConsumer(new DirectChannel(), "input");
bindingService.bindProducer(new DirectChannel(), "output");
}
@SuppressWarnings("unchecked")
@Test
public void testUnrecognizedBinderDisallowedIfUsed() {
HashMap<String, String> properties = new HashMap<>();
@@ -560,7 +563,7 @@ public class BindingServiceTests {
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
properties);
BinderFactory binderFactory = new BindingServiceConfiguration()
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties);
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class));
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory);
bindingService.bindConsumer(new DirectChannel(), "input");
@@ -693,7 +696,7 @@ public class BindingServiceTests {
return new DefaultBinderFactory(
Collections.singletonMap("mock",
new BinderConfiguration("mock", new HashMap<>(), true, true)),
binderTypeRegistry);
binderTypeRegistry, null);
}
private DefaultBinderTypeRegistry createMockBinderTypeRegistry() {