INT-4072 Fix applySequence with State Propagation
JIRA: https://jira.spring.io/browse/INT-4072 When `publishSubscribeChannel` is with `applySequence = true`, a `messageToSend` is overridden with `sequenceDetails` using `MessageBuilder`, therefore a new fresh `Message`. In case of state propagation, e.g. `SecurityContextPropagationChannelInterceptor`, we just lost the state from the `ThreadStatePropagationChannelInterceptor.MessageWithThreadState` because of new `Message<?>` * Add into `BroadcastingDispatcher` the logic to delegate `pushSequenceDetails` into `MessageWithThreadState` directly do not lose the `state` * Make `ThreadStatePropagationChannelInterceptor` as `MessageBuilderFactory`-aware and use it to rebuild an `original` `Message<?>` in the `MessageWithThreadState` to populate `SequenceDetails` **Cherry-pick to 4.2.x** Provide an explicit order for `publishSubscribeChannel` subscribers Fixes GH-1847 (https://github.com/spring-projects/spring-integration/issues/1847) Fix mutation in the `ThreadStatePropagationChannelInterceptor` Since `BroadcastingDispatcher` invokes `pushSequenceDetails` for each subscribed handler, make `MessageWithThreadState` as immutable and return a new instance via `cloneWithSequenceDetails()` method with particular `sequenceDetails`. Previous mutable solution ended up with the issue of concurrent modification. * Introduce `CloneableMessage` abstraction to let any custom `Message` to return `MessageBuilder` with desired context. * Introduce `DelegatingMessageBuilder` as an extension of the `MessageBuilder` to let custom `CloneableMessage` to return desired customization. * Add into `MessageBuilder#fromMessage()` `if` for the `CloneableMessage` * Add into `MutableMessageBuilder` a `warn` about `CloneableMessage` * Revert changes in the `BroadcastingDispatcher` in favor of `CloneableMessage` in the `MessageBuilder` * Redo `ThreadStatePropagationChannelInterceptor#MessageWithThreadState` logic to be based on the `CloneableMessage` and `DelegatingMessageBuilder` extension. Introduce `MessageDecorator` contract Remove `CloneableMessage` aspect and everything around `MessageWithThreadState` is now `MessageDecorator` and `BroadcastingDispatcher` check if incoming `message` is `MessageDecorator` and performs its `decorateMessage` after `builder`
This commit is contained in:
committed by
Gary Russell
parent
bce851576e
commit
999644a530
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.integration.channel.interceptor;
|
||||
|
||||
import org.springframework.integration.support.MessageDecorator;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
@@ -91,14 +92,15 @@ public abstract class ThreadStatePropagationChannelInterceptor<S>
|
||||
protected abstract void populatePropagatedContext(S state, Message<?> message, MessageChannel channel);
|
||||
|
||||
|
||||
private static final class MessageWithThreadState<S> implements Message<Object> {
|
||||
private static final class MessageWithThreadState<S> implements Message<Object>, MessageDecorator {
|
||||
|
||||
private final Message<?> message;
|
||||
private final Message<Object> message;
|
||||
|
||||
private final S state;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private MessageWithThreadState(Message<?> message, S state) {
|
||||
this.message = message;
|
||||
this.message = (Message<Object>) message;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@@ -112,6 +114,11 @@ public abstract class ThreadStatePropagationChannelInterceptor<S>
|
||||
return this.message.getHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> decorateMessage(Message<?> message) {
|
||||
return new MessageWithThreadState<S>(message, this.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MessageWithThreadState{" +
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.integration.dispatcher;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.support.DefaultMessageBuilderFactory;
|
||||
import org.springframework.integration.support.MessageBuilderFactory;
|
||||
import org.springframework.integration.support.MessageDecorator;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
@@ -158,13 +160,20 @@ public class BroadcastingDispatcher extends AbstractDispatcher implements BeanFa
|
||||
throw new MessageDispatchingException(message, "Dispatcher has no subscribers");
|
||||
}
|
||||
int sequenceSize = handlers.size();
|
||||
Message<?> messageToSend = message;
|
||||
UUID sequenceId = null;
|
||||
if (this.applySequence) {
|
||||
sequenceId = message.getHeaders().getId();
|
||||
}
|
||||
for (MessageHandler handler : handlers) {
|
||||
Message<?> messageToSend = message;
|
||||
if (this.applySequence) {
|
||||
messageToSend = getMessageBuilderFactory()
|
||||
.fromMessage(message)
|
||||
.pushSequenceDetails(message.getHeaders().getId(), sequenceNumber++, sequenceSize)
|
||||
.pushSequenceDetails(sequenceId, sequenceNumber++, sequenceSize)
|
||||
.build();
|
||||
if (message instanceof MessageDecorator) {
|
||||
messageToSend = ((MessageDecorator) message).decorateMessage(messageToSend);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.executor != null) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.integration.support;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* The {@link Message} decoration contract.
|
||||
* An implementation may decide to return any {@link Message} instance
|
||||
* and even a different {@link Message} implementation. Usually used to
|
||||
* wrap a message in another.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
* @since 4.2.9
|
||||
*/
|
||||
public interface MessageDecorator {
|
||||
|
||||
Message<?> decorateMessage(Message<?> message);
|
||||
|
||||
}
|
||||
@@ -33,13 +33,17 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.annotation.BridgeTo;
|
||||
import org.springframework.integration.annotation.Poller;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.ExecutorChannel;
|
||||
import org.springframework.integration.channel.PublishSubscribeChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.config.GlobalChannelInterceptor;
|
||||
import org.springframework.integration.handler.BridgeHandler;
|
||||
import org.springframework.integration.security.SecurityTestUtils;
|
||||
import org.springframework.integration.security.TestHandler;
|
||||
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
|
||||
@@ -47,6 +51,7 @@ import org.springframework.integration.security.channel.SecuredChannel;
|
||||
import org.springframework.integration.security.channel.SecurityContextPropagationChannelInterceptor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
@@ -95,6 +100,14 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
@Qualifier("executorChannel")
|
||||
MessageChannel executorChannel;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("publishSubscribeChannel")
|
||||
PublishSubscribeChannel publishSubscribeChannel;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("securedChannelQueue2")
|
||||
PollableChannel securedChannelQueue2;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("errorChannel")
|
||||
PollableChannel errorChannel;
|
||||
@@ -170,7 +183,7 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
this.queueChannel.send(new GenericMessage<String>("test"));
|
||||
Message<?> errorMessage = this.errorChannel.receive(1000);
|
||||
Message<?> errorMessage = this.errorChannel.receive(10000);
|
||||
assertNotNull(errorMessage);
|
||||
Object payload = errorMessage.getPayload();
|
||||
assertThat(payload, instanceOf(MessageHandlingException.class));
|
||||
@@ -187,8 +200,8 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
this.queueChannel.send(new GenericMessage<String>("test"));
|
||||
Message<?> errorMessage = this.errorChannel.receive(1000);
|
||||
this.executorChannel.send(new GenericMessage<String>("test"));
|
||||
Message<?> errorMessage = this.errorChannel.receive(10000);
|
||||
assertNotNull(errorMessage);
|
||||
Object payload = errorMessage.getPayload();
|
||||
assertThat(payload, instanceOf(MessageHandlingException.class));
|
||||
@@ -196,6 +209,48 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
instanceOf(AuthenticationCredentialsNotFoundException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecurityContextPropagationPublishSubscribeChannel() {
|
||||
login("bob", "bobspassword", "ROLE_ADMIN", "ROLE_PRESIDENT");
|
||||
|
||||
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
|
||||
|
||||
Message<?> receive = this.securedChannelQueue.receive(10000);
|
||||
assertNotNull(receive);
|
||||
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(receive);
|
||||
assertEquals(new Integer(0), headerAccessor.getSequenceNumber());
|
||||
|
||||
receive = this.securedChannelQueue2.receive(10000);
|
||||
assertNotNull(receive);
|
||||
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
|
||||
assertEquals(new Integer(0), headerAccessor.getSequenceNumber());
|
||||
|
||||
this.publishSubscribeChannel.setApplySequence(true);
|
||||
|
||||
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
|
||||
|
||||
receive = this.securedChannelQueue.receive(10000);
|
||||
assertNotNull(receive);
|
||||
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
|
||||
assertEquals(new Integer(1), headerAccessor.getSequenceNumber());
|
||||
|
||||
receive = this.securedChannelQueue2.receive(10000);
|
||||
assertNotNull(receive);
|
||||
headerAccessor = new IntegrationMessageHeaderAccessor(receive);
|
||||
assertEquals(new Integer(2), headerAccessor.getSequenceNumber());
|
||||
|
||||
this.publishSubscribeChannel.setApplySequence(false);
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
this.publishSubscribeChannel.send(new GenericMessage<String>("test"));
|
||||
Message<?> errorMessage = this.errorChannel.receive(10000);
|
||||
assertNotNull(errorMessage);
|
||||
Object payload = errorMessage.getPayload();
|
||||
assertThat(payload, instanceOf(MessageHandlingException.class));
|
||||
assertThat(((MessageHandlingException) payload).getCause(),
|
||||
instanceOf(AuthenticationCredentialsNotFoundException.class));
|
||||
}
|
||||
|
||||
private void login(String username, String password, String... roles) {
|
||||
SecurityContext context = SecurityTestUtils.createContext(username, password, roles);
|
||||
@@ -231,7 +286,10 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@GlobalChannelInterceptor(patterns = {"#{'queueChannel'}", "${security.channel:executorChannel}"})
|
||||
@GlobalChannelInterceptor(patterns = {
|
||||
"#{'queueChannel'}",
|
||||
"${security.channel:executorChannel}",
|
||||
"publishSubscribeChannel" })
|
||||
public ChannelInterceptor securityContextPropagationInterceptor() {
|
||||
return new SecurityContextPropagationChannelInterceptor();
|
||||
}
|
||||
@@ -255,6 +313,35 @@ public class ChannelSecurityInterceptorSecuredChannelAnnotationTests {
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public PublishSubscribeChannel publishSubscribeChannel() {
|
||||
return new PublishSubscribeChannel(Executors.newCachedThreadPool());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ServiceActivator(inputChannel = "publishSubscribeChannel")
|
||||
public MessageHandler securedChannelQueueBridge() {
|
||||
BridgeHandler handler = new BridgeHandler();
|
||||
handler.setOutputChannel(securedChannelQueue());
|
||||
handler.setOrder(1);
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SecuredChannel(interceptor = "channelSecurityInterceptor", sendAccess = {"ROLE_ADMIN", "ROLE_PRESIDENT"})
|
||||
public PollableChannel securedChannelQueue2() {
|
||||
return new QueueChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ServiceActivator(inputChannel = "publishSubscribeChannel")
|
||||
public MessageHandler securedChannelQueue2Bridge() {
|
||||
BridgeHandler handler = new BridgeHandler();
|
||||
handler.setOutputChannel(securedChannelQueue2());
|
||||
handler.setOrder(2);
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
return new ThreadPoolTaskScheduler();
|
||||
|
||||
Reference in New Issue
Block a user