Unbind dynamic producers
- Add `SmartLifecycle` to BinderAwareChannelResolver - Add resolved destinations' bindings to the list of `Bindings` that need to be onbound - Add lowest phase for its lifecycle so that bindings are unbound at the latest This resolves #370 Use a BindableAdapter to register the output channels - Add `DyanamicBindable` which is a BindableAdapter that stores the dynamic destination names and handle their unbinding - Inject `ChannelBindingService` into BinderAwareChannelResolver so that the dynamic binding gets registered with `producerBindings` and can subsequently be used when unbinding - Fix the `BinderAwareChannelResolver` to resolve the `transport:name` correctly by setting the bean name of the channel to use `name` only (without including transport:) - Update tests Use Binding to unbind instead of ChannelBindingService
This commit is contained in:
committed by
Marius Bogoevici
parent
41a24a13e6
commit
66a78bf685
@@ -35,8 +35,10 @@ import org.springframework.util.ObjectUtils;
|
||||
* resolves the channel from the bean factory and, if not present, creates a new channel
|
||||
* and adds it to the factory after binding it to the binder. The binder is optionally
|
||||
* determined with a prefix preceding a colon.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author Ilayaperumal Gopinathan
|
||||
*/
|
||||
public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestinationResolver {
|
||||
|
||||
@@ -44,13 +46,16 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina
|
||||
|
||||
private final ChannelBindingServiceProperties channelBindingServiceProperties;
|
||||
|
||||
private final DynamicBindable dynamicBindable;
|
||||
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
public BinderAwareChannelResolver(BinderFactory<MessageChannel> binderFactory,
|
||||
ChannelBindingServiceProperties channelBindingServiceProperties) {
|
||||
public BinderAwareChannelResolver(BinderFactory binderFactory,
|
||||
ChannelBindingServiceProperties channelBindingServiceProperties, DynamicBindable dynamicBindable) {
|
||||
Assert.notNull(binderFactory, "'binderFactory' cannot be null");
|
||||
this.binderFactory = binderFactory;
|
||||
this.channelBindingServiceProperties = channelBindingServiceProperties;
|
||||
this.dynamicBindable = dynamicBindable;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,49 +67,45 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageChannel resolveDestination(String name) {
|
||||
public MessageChannel resolveDestination(String destinationName) {
|
||||
MessageChannel channel = null;
|
||||
DestinationResolutionException destinationResolutionException;
|
||||
try {
|
||||
return super.resolveDestination(name);
|
||||
return super.resolveDestination(destinationName);
|
||||
}
|
||||
catch (DestinationResolutionException e) {
|
||||
destinationResolutionException = e;
|
||||
}
|
||||
synchronized (this) {
|
||||
try {
|
||||
return super.resolveDestination(name);
|
||||
}
|
||||
catch (DestinationResolutionException e) {
|
||||
destinationResolutionException = e;
|
||||
}
|
||||
if (this.beanFactory != null && this.binderFactory != null) {
|
||||
String[] dynamicDestinations = null;
|
||||
Properties producerProperties = null;
|
||||
if (this.channelBindingServiceProperties != null) {
|
||||
dynamicDestinations = this.channelBindingServiceProperties.getDynamicDestinations();
|
||||
// TODO: need the props to return some defaults if not found
|
||||
producerProperties = this.channelBindingServiceProperties.getProducerProperties(name);
|
||||
producerProperties = this.channelBindingServiceProperties.getProducerProperties(destinationName);
|
||||
}
|
||||
boolean dynamicAllowed = ObjectUtils.isEmpty(dynamicDestinations)
|
||||
|| ObjectUtils.containsElement(dynamicDestinations, name);
|
||||
|| ObjectUtils.containsElement(dynamicDestinations, destinationName);
|
||||
if (dynamicAllowed) {
|
||||
channel = new DirectChannel();
|
||||
this.beanFactory.registerSingleton(name, channel);
|
||||
channel = (MessageChannel) this.beanFactory.initializeBean(channel, name);
|
||||
String transport = null;
|
||||
if (name.contains(":")) {
|
||||
String[] tokens = name.split(":", 2);
|
||||
String beanName = destinationName;
|
||||
if (destinationName.contains(":")) {
|
||||
String[] tokens = destinationName.split(":", 2);
|
||||
if (tokens.length == 2) {
|
||||
transport = tokens[0];
|
||||
destinationName = tokens[1];
|
||||
}
|
||||
else if (tokens.length != 1) {
|
||||
throw new IllegalArgumentException("Unrecognized channel naming scheme: " + name + " , should be" +
|
||||
" [<transport>:]<name>");
|
||||
throw new IllegalArgumentException("Unrecognized channel naming scheme: " + destinationName + " , should be" +
|
||||
" [<transport>:]<destinationName>");
|
||||
}
|
||||
}
|
||||
channel = new DirectChannel();
|
||||
this.beanFactory.registerSingleton(beanName, channel);
|
||||
channel = (MessageChannel) this.beanFactory.initializeBean(channel, beanName);
|
||||
Binder<MessageChannel> binder = binderFactory.getBinder(transport);
|
||||
binder.bindProducer(name, channel, producerProperties);
|
||||
this.dynamicBindable.addDynamicOutputs(beanName, binder.bindProducer(destinationName, channel, producerProperties));
|
||||
}
|
||||
else {
|
||||
throw destinationResolutionException;
|
||||
@@ -113,5 +114,4 @@ public class BinderAwareChannelResolver extends BeanFactoryMessageChannelDestina
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2016 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
|
||||
*
|
||||
* http://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.binding;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.cloud.stream.binder.Binding;
|
||||
|
||||
/**
|
||||
* A {@link BindableAdapter} that stores the dynamic destination names and handle their unbinding.
|
||||
*
|
||||
* @author Ilayaperumal Gopinathan
|
||||
*/
|
||||
public final class DynamicBindable extends BindableAdapter {
|
||||
|
||||
/**
|
||||
* Map containing dynamic destination names and their bindings.
|
||||
*/
|
||||
private Map<String, Binding> outputBindings = new HashMap<>();
|
||||
|
||||
void addDynamicOutputs(String name, Binding binding) {
|
||||
this.outputBindings.put(name, binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getOutputs() {
|
||||
return Collections.unmodifiableSet(outputBindings.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindOutputs(ChannelBindingService adapter) {
|
||||
for (Map.Entry<String, Binding> entry: outputBindings.entrySet()) {
|
||||
entry.getValue().unbind();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import org.springframework.cloud.stream.binding.ChannelBindingService;
|
||||
import org.springframework.cloud.stream.binding.CompositeMessageChannelConfigurer;
|
||||
import org.springframework.cloud.stream.binding.ContextStartAfterRefreshListener;
|
||||
import org.springframework.cloud.stream.binding.DefaultBindableChannelFactory;
|
||||
import org.springframework.cloud.stream.binding.DynamicBindable;
|
||||
import org.springframework.cloud.stream.binding.InputBindingLifecycle;
|
||||
import org.springframework.cloud.stream.binding.MessageChannelConfigurer;
|
||||
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
|
||||
@@ -134,10 +135,9 @@ public class ChannelBindingServiceConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BinderAwareChannelResolver binderAwareChannelResolver(
|
||||
BinderFactory<MessageChannel> binderFactory,
|
||||
public BinderAwareChannelResolver binderAwareChannelResolver(BinderFactory<MessageChannel> binderFactory,
|
||||
ChannelBindingServiceProperties channelBindingServiceProperties) {
|
||||
return new BinderAwareChannelResolver(binderFactory, channelBindingServiceProperties);
|
||||
return new BinderAwareChannelResolver(binderFactory, channelBindingServiceProperties, dynamicBindable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -153,6 +153,11 @@ public class ChannelBindingServiceConfiguration {
|
||||
return new SingleChannelBindable(ERROR_CHANNEL_NAME, errorChannel);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DynamicBindable dynamicBindable() {
|
||||
return new DynamicBindable();
|
||||
}
|
||||
|
||||
// IMPORTANT: Nested class to avoid instantiating all of the above early
|
||||
@Configuration
|
||||
protected static class PostProcessorConfiguration {
|
||||
|
||||
@@ -21,8 +21,8 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.matches;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -43,6 +43,7 @@ import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.cloud.stream.binding.BinderAwareChannelResolver;
|
||||
import org.springframework.cloud.stream.binding.DynamicBindable;
|
||||
import org.springframework.cloud.stream.config.BindingProperties;
|
||||
import org.springframework.cloud.stream.config.ChannelBindingServiceProperties;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.messaging.SubscribableChannel;
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Gary Russell
|
||||
* @author Ilayaperumal Gopinathan
|
||||
*/
|
||||
public class BinderAwareChannelResolverTests {
|
||||
|
||||
@@ -71,12 +73,13 @@ public class BinderAwareChannelResolverTests {
|
||||
@Before
|
||||
public void setupContext() throws Exception {
|
||||
this.binder = new TestBinder();
|
||||
this.resolver = new BinderAwareChannelResolver(new BinderFactory<MessageChannel>() {
|
||||
BinderFactory binderFactory = new BinderFactory<MessageChannel>() {
|
||||
@Override
|
||||
public Binder<MessageChannel> getBinder(String configurationName) {
|
||||
return binder;
|
||||
}
|
||||
}, null);
|
||||
};
|
||||
this.resolver = new BinderAwareChannelResolver(binderFactory, null, new DynamicBindable());
|
||||
this.resolver.setBeanFactory(context.getBeanFactory());
|
||||
context.getBeanFactory().registerSingleton("channelResolver",
|
||||
this.resolver);
|
||||
@@ -125,6 +128,7 @@ public class BinderAwareChannelResolverTests {
|
||||
@SuppressWarnings("rawtypes")
|
||||
public void propertyPassthrough() {
|
||||
ChannelBindingServiceProperties bindingServiceProperties = new ChannelBindingServiceProperties();
|
||||
DynamicBindable dynamicBindable = new DynamicBindable();
|
||||
Map<String, BindingProperties> bindings = new HashMap<String, BindingProperties>();
|
||||
BindingProperties bindingProperties = new BindingProperties();
|
||||
bindingProperties.setContentType("text/plain");
|
||||
@@ -132,19 +136,30 @@ public class BinderAwareChannelResolverTests {
|
||||
bindingServiceProperties.setBindings(bindings);
|
||||
@SuppressWarnings("unchecked")
|
||||
Binder<MessageChannel> binder = mock(Binder.class);
|
||||
Binder<MessageChannel> binder2 = mock(Binder.class);
|
||||
BinderFactory mockBinderFactory = Mockito.mock(BinderFactory.class);
|
||||
when(mockBinderFactory.getBinder(anyString())).thenReturn(binder);
|
||||
Binding<MessageChannel> fooBinding = Mockito.mock(Binding.class);
|
||||
Binding<MessageChannel> barBinding = Mockito.mock(Binding.class);
|
||||
when(binder.bindProducer(
|
||||
matches("foo"), any(DirectChannel.class), any(Properties.class))).thenReturn(fooBinding);
|
||||
when(binder2.bindProducer(
|
||||
matches("bar"), any(DirectChannel.class), any(Properties.class))).thenReturn(barBinding);
|
||||
when(mockBinderFactory.getBinder(null)).thenReturn(binder);
|
||||
when(mockBinderFactory.getBinder("someTransport")).thenReturn(binder2);
|
||||
@SuppressWarnings("unchecked")
|
||||
BinderAwareChannelResolver resolver =
|
||||
new BinderAwareChannelResolver(mockBinderFactory, bindingServiceProperties);
|
||||
new BinderAwareChannelResolver(mockBinderFactory, bindingServiceProperties, dynamicBindable);
|
||||
BeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
resolver.setBeanFactory(beanFactory);
|
||||
MessageChannel resolved = resolver.resolveDestination("foo");
|
||||
verify(binder).bindProducer(eq("foo"), any(MessageChannel.class), any(Properties.class));
|
||||
assertSame(resolved, beanFactory.getBean("foo"));
|
||||
resolved = resolver.resolveDestination("someTransport:foo");
|
||||
verify(binder).bindProducer(eq("someTransport:foo"), any(MessageChannel.class), any(Properties.class));
|
||||
assertSame(resolved, beanFactory.getBean("someTransport:foo"));
|
||||
resolved = resolver.resolveDestination("someTransport:bar");
|
||||
verify(binder2).bindProducer(eq("bar"), any(MessageChannel.class), any(Properties.class));
|
||||
assertSame(resolved, beanFactory.getBean("someTransport:bar"));
|
||||
assertTrue("Dynamic bindable should have two destination names", dynamicBindable.getOutputs().size() == 2);
|
||||
assertTrue("Dynamic bindable should have the destination name 'foo'", dynamicBindable.getOutputs().contains("foo"));
|
||||
assertTrue("Dynamic bindable should have the destination name 'bar'", dynamicBindable.getOutputs().contains("someTransport:bar"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +171,7 @@ public class BinderAwareChannelResolverTests {
|
||||
|
||||
@Override
|
||||
public Binding<MessageChannel> bindConsumer(String name, String group, MessageChannel inboundBindTarget,
|
||||
Properties properties) {
|
||||
Properties properties) {
|
||||
synchronized (destinations) {
|
||||
if (!destinations.containsKey(name)) {
|
||||
destinations.put(name, new DirectChannel());
|
||||
|
||||
@@ -61,6 +61,7 @@ import org.springframework.messaging.core.DestinationResolutionException;
|
||||
* @author Gary Russell
|
||||
* @author Mark Fisher
|
||||
* @author Marius Bogoevici
|
||||
* @author Ilayaperumal Gopinathan
|
||||
*/
|
||||
public class ChannelBindingServiceTests {
|
||||
|
||||
@@ -181,6 +182,7 @@ public class ChannelBindingServiceTests {
|
||||
public void checkDynamicBinding () {
|
||||
|
||||
ChannelBindingServiceProperties properties = new ChannelBindingServiceProperties();
|
||||
DynamicBindable dynamicBindable = new DynamicBindable();
|
||||
DefaultBinderFactory<MessageChannel> binderFactory =
|
||||
new DefaultBinderFactory<>(Collections.singletonMap("mock",
|
||||
new BinderConfiguration(new BinderType("mock", new Class[]{MockBinderConfiguration.class}),
|
||||
@@ -193,8 +195,8 @@ public class ChannelBindingServiceTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
final AtomicReference<MessageChannel> dynamic = new AtomicReference<>();
|
||||
when(binder.bindProducer(
|
||||
matches("mock:bar"), any(DirectChannel.class), any(Properties.class))).thenReturn(mockBinding);
|
||||
BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(binderFactory, properties);
|
||||
matches("bar"), any(DirectChannel.class), any(Properties.class))).thenReturn(mockBinding);
|
||||
BinderAwareChannelResolver resolver = new BinderAwareChannelResolver(binderFactory, properties, dynamicBindable);
|
||||
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);
|
||||
when(beanFactory.getBean("mock:bar", MessageChannel.class))
|
||||
.thenThrow(new NoSuchBeanDefinitionException(MessageChannel.class));
|
||||
@@ -206,7 +208,7 @@ public class ChannelBindingServiceTests {
|
||||
return null;
|
||||
}
|
||||
|
||||
}).when(beanFactory).registerSingleton(eq("mock:bar"), any(MessageChannel.class));
|
||||
}).when(beanFactory).registerSingleton(eq("bar"), any(MessageChannel.class));
|
||||
doAnswer(new Answer<Object>() {
|
||||
|
||||
@Override
|
||||
@@ -214,11 +216,11 @@ public class ChannelBindingServiceTests {
|
||||
return dynamic.get();
|
||||
}
|
||||
|
||||
}).when(beanFactory).initializeBean(any(MessageChannel.class), eq("mock:bar"));
|
||||
}).when(beanFactory).initializeBean(any(MessageChannel.class), eq("bar"));
|
||||
resolver.setBeanFactory(beanFactory);
|
||||
MessageChannel resolved = resolver.resolveDestination("mock:bar");
|
||||
assertThat(resolved, sameInstance(dynamic.get()));
|
||||
verify(binder).bindProducer(eq("mock:bar"), eq(dynamic.get()), any(Properties.class));
|
||||
verify(binder).bindProducer(eq("bar"), eq(dynamic.get()), any(Properties.class));
|
||||
properties.setDynamicDestinations(new String[] { "mock:bar" });
|
||||
resolved = resolver.resolveDestination("mock:bar");
|
||||
assertThat(resolved, sameInstance(dynamic.get()));
|
||||
|
||||
Reference in New Issue
Block a user