GH-7925: Make message history header as mutable
Fixes: #7925 The `MessageHistory.write()` creates not only a new instance of the `MessageHistory`, but also a new copy of the whole message. This significantly impacts the performance when we have too many components to track * Make `MessageHistory` as append-only container and create a new instance (plus message) only on the first track when no prior history is present * Change `WireTap`, `BroadcastingDispatcher`, `AbstractMessageRouter` and `AbstractMessageSplitter` to use a new `AbstractIntegrationMessageBuilder.cloneMessageHistoryIfAny()` API for every branch a message is produced. Essentially, create a new message with copy of the message history to let that downstream sub-flow have its own trace * Modify failed unit tests for a new logic where message history header is not immutable anymore * This also fixes an `AbstractMessageSplitter` for propagating its track into messages it emits * * Do not clone message history header if only one consume in multi-publish * Fix typos in docs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -23,8 +23,11 @@ import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.integration.core.MessageSelector;
|
||||
import org.springframework.integration.history.MessageHistory;
|
||||
import org.springframework.integration.support.MessageBuilderFactory;
|
||||
import org.springframework.integration.support.channel.ChannelResolverUtils;
|
||||
import org.springframework.integration.support.management.ManageableLifecycle;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
@@ -57,6 +60,8 @@ public class WireTap implements ChannelInterceptor, ManageableLifecycle, VetoCap
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private MessageBuilderFactory messageBuilderFactory;
|
||||
|
||||
private volatile boolean running = true;
|
||||
|
||||
|
||||
@@ -162,9 +167,19 @@ public class WireTap implements ChannelInterceptor, ManageableLifecycle, VetoCap
|
||||
return message;
|
||||
}
|
||||
if (this.running && (this.selector == null || this.selector.accept(message))) {
|
||||
boolean sent = (this.timeout >= 0)
|
||||
? wireTapChannel.send(message, this.timeout)
|
||||
: wireTapChannel.send(message);
|
||||
Message<?> messageToSend = message;
|
||||
if (message.getHeaders().containsKey(MessageHistory.HEADER_NAME)) {
|
||||
messageToSend =
|
||||
getMessageBuilderFactory()
|
||||
.fromMessage(message)
|
||||
.cloneMessageHistoryIfAny()
|
||||
.build();
|
||||
}
|
||||
boolean sent =
|
||||
(this.timeout >= 0)
|
||||
? wireTapChannel.send(messageToSend, this.timeout)
|
||||
: wireTapChannel.send(messageToSend);
|
||||
|
||||
if (!sent && LOGGER.isWarnEnabled()) {
|
||||
LOGGER.warn("failed to send message to WireTap channel '" + wireTapChannel + "'");
|
||||
}
|
||||
@@ -174,7 +189,6 @@ public class WireTap implements ChannelInterceptor, ManageableLifecycle, VetoCap
|
||||
|
||||
@Override
|
||||
public boolean shouldIntercept(String beanName, InterceptableChannel channel) {
|
||||
|
||||
return !getChannel().equals(channel);
|
||||
}
|
||||
|
||||
@@ -189,4 +203,11 @@ public class WireTap implements ChannelInterceptor, ManageableLifecycle, VetoCap
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
private MessageBuilderFactory getMessageBuilderFactory() {
|
||||
if (this.messageBuilderFactory == null) {
|
||||
this.messageBuilderFactory = IntegrationUtils.getMessageBuilderFactory(this.beanFactory);
|
||||
}
|
||||
return this.messageBuilderFactory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -17,18 +17,23 @@
|
||||
package org.springframework.integration.dispatcher;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.context.IntegrationObjectSupport;
|
||||
import org.springframework.integration.history.MessageHistory;
|
||||
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
|
||||
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;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.MessageHandlingRunnable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -153,13 +158,28 @@ public class BroadcastingDispatcher extends AbstractDispatcher implements BeanFa
|
||||
}
|
||||
int sequenceSize = handlers.size();
|
||||
Message<?> messageToSend = message;
|
||||
MessageHeaders messageHeaders = message.getHeaders();
|
||||
UUID correlationKey = messageHeaders.getId();
|
||||
boolean hasMessageHistory = messageHeaders.containsKey(MessageHistory.HEADER_NAME) && sequenceSize > 1;
|
||||
for (MessageHandler handler : handlers) {
|
||||
if (this.applySequence) {
|
||||
messageToSend = getMessageBuilderFactory()
|
||||
.fromMessage(message)
|
||||
.pushSequenceDetails(message.getHeaders().getId(), sequenceNumber++, sequenceSize)
|
||||
.build();
|
||||
if (this.applySequence || hasMessageHistory) {
|
||||
AbstractIntegrationMessageBuilder<?> builder =
|
||||
getMessageBuilderFactory()
|
||||
.fromMessage(message);
|
||||
|
||||
if (this.applySequence) {
|
||||
builder.pushSequenceDetails(
|
||||
correlationKey == null ? IntegrationObjectSupport.generateId() : correlationKey,
|
||||
sequenceNumber++, sequenceSize);
|
||||
}
|
||||
|
||||
if (hasMessageHistory) {
|
||||
builder.cloneMessageHistoryIfAny();
|
||||
}
|
||||
|
||||
messageToSend = builder.build();
|
||||
}
|
||||
|
||||
if (message instanceof MessageDecorator messageDecorator) {
|
||||
messageToSend = messageDecorator.decorateMessage(messageToSend);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -465,6 +466,13 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
|
||||
}
|
||||
else {
|
||||
builder = getMessageBuilderFactory().withPayload(output);
|
||||
// Assuming that message in the payload collection is a copy of request message.
|
||||
if (output instanceof Iterable<?> iterable) {
|
||||
Iterator<?> iterator = iterable.iterator();
|
||||
if (iterator.hasNext() && iterator.next() instanceof Message<?>) {
|
||||
builder = builder.cloneMessageHistoryIfAny();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.noHeadersPropagation &&
|
||||
(shouldCopyRequestHeaders() ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.integration.history;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -52,8 +53,9 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public final class MessageHistory implements List<Properties>, Serializable {
|
||||
public final class MessageHistory implements List<Properties>, Serializable, Cloneable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2340400235574314134L;
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(MessageHistory.class);
|
||||
@@ -94,51 +96,52 @@ public final class MessageHistory implements List<Properties>, Serializable {
|
||||
Assert.notNull(component, "Component must not be null");
|
||||
Properties metadata = extractMetadata(component);
|
||||
if (!metadata.isEmpty()) {
|
||||
MessageHistory previousHistory = message.getHeaders().get(HEADER_NAME, MessageHistory.class);
|
||||
List<Properties> components =
|
||||
previousHistory != null
|
||||
? new ArrayList<>(previousHistory)
|
||||
: new ArrayList<>();
|
||||
components.add(metadata);
|
||||
MessageHistory history = new MessageHistory(components);
|
||||
|
||||
if (message instanceof MutableMessage) {
|
||||
message.getHeaders().put(HEADER_NAME, history);
|
||||
}
|
||||
else if (message instanceof ErrorMessage) {
|
||||
ErrorMessage errorMessage = (ErrorMessage) message;
|
||||
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(message);
|
||||
headerAccessor.setHeader(HEADER_NAME, history);
|
||||
Throwable payload = errorMessage.getPayload();
|
||||
Message<?> originalMessage = errorMessage.getOriginalMessage();
|
||||
if (originalMessage != null) {
|
||||
errorMessage = new ErrorMessage(payload, headerAccessor.toMessageHeaders(), originalMessage);
|
||||
}
|
||||
else {
|
||||
errorMessage = new ErrorMessage(payload, headerAccessor.toMessageHeaders());
|
||||
}
|
||||
message = (Message<T>) errorMessage;
|
||||
}
|
||||
else if (message instanceof AdviceMessage) {
|
||||
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(message);
|
||||
headerAccessor.setHeader(HEADER_NAME, history);
|
||||
message = new AdviceMessage<T>(message.getPayload(), headerAccessor.toMessageHeaders(),
|
||||
((AdviceMessage<?>) message).getInputMessage());
|
||||
MessageHistory messageHistory = message.getHeaders().get(HEADER_NAME, MessageHistory.class);
|
||||
if (messageHistory != null) {
|
||||
messageHistory.components.add(metadata);
|
||||
}
|
||||
else {
|
||||
if (!(message instanceof GenericMessage) &&
|
||||
(messageBuilderFactory instanceof DefaultMessageBuilderFactory ||
|
||||
messageBuilderFactory instanceof MutableMessageBuilderFactory)
|
||||
&& LOGGER.isWarnEnabled()) {
|
||||
List<Properties> components = new ArrayList<>();
|
||||
components.add(metadata);
|
||||
messageHistory = new MessageHistory(components);
|
||||
|
||||
LOGGER.warn("MessageHistory rebuilds the message and produces the result of the [" +
|
||||
messageBuilderFactory + "], not an instance of the provided type [" +
|
||||
message.getClass() + "]. Consider to supply a custom MessageBuilderFactory " +
|
||||
"to retain custom messages during MessageHistory tracking.");
|
||||
if (message instanceof MutableMessage) {
|
||||
message.getHeaders().put(HEADER_NAME, messageHistory);
|
||||
}
|
||||
else if (message instanceof ErrorMessage errorMessage) {
|
||||
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(message);
|
||||
headerAccessor.setHeader(HEADER_NAME, messageHistory);
|
||||
Throwable payload = errorMessage.getPayload();
|
||||
Message<?> originalMessage = errorMessage.getOriginalMessage();
|
||||
if (originalMessage != null) {
|
||||
errorMessage = new ErrorMessage(payload, headerAccessor.toMessageHeaders(), originalMessage);
|
||||
}
|
||||
else {
|
||||
errorMessage = new ErrorMessage(payload, headerAccessor.toMessageHeaders());
|
||||
}
|
||||
message = (Message<T>) errorMessage;
|
||||
}
|
||||
else if (message instanceof AdviceMessage<?> adviceMessage) {
|
||||
IntegrationMessageHeaderAccessor headerAccessor = new IntegrationMessageHeaderAccessor(message);
|
||||
headerAccessor.setHeader(HEADER_NAME, messageHistory);
|
||||
message = new AdviceMessage<>(message.getPayload(), headerAccessor.toMessageHeaders(),
|
||||
adviceMessage.getInputMessage());
|
||||
}
|
||||
else {
|
||||
if (!(message instanceof GenericMessage) &&
|
||||
(messageBuilderFactory instanceof DefaultMessageBuilderFactory ||
|
||||
messageBuilderFactory instanceof MutableMessageBuilderFactory)
|
||||
&& LOGGER.isWarnEnabled()) {
|
||||
|
||||
LOGGER.warn("MessageHistory rebuilds the message and produces the result of the [" +
|
||||
messageBuilderFactory + "], not an instance of the provided type [" +
|
||||
message.getClass() + "]. Consider to supply a custom MessageBuilderFactory " +
|
||||
"to retain custom messages during MessageHistory tracking.");
|
||||
}
|
||||
message = messageBuilderFactory.fromMessage(message)
|
||||
.setHeader(HEADER_NAME, messageHistory)
|
||||
.build();
|
||||
}
|
||||
message = messageBuilderFactory.fromMessage(message)
|
||||
.setHeader(HEADER_NAME, history)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
@@ -216,15 +219,19 @@ public final class MessageHistory implements List<Properties>, Serializable {
|
||||
return this.components.lastIndexOf(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return new MessageHistory(new ArrayList<>(this.components));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof MessageHistory)) {
|
||||
if (!(o instanceof MessageHistory that)) {
|
||||
return false;
|
||||
}
|
||||
MessageHistory that = (MessageHistory) o;
|
||||
return this.components.equals(that.components);
|
||||
}
|
||||
|
||||
@@ -318,6 +325,7 @@ public final class MessageHistory implements List<Properties>, Serializable {
|
||||
*/
|
||||
public static class Entry extends Properties {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -8225834391885601079L;
|
||||
|
||||
public String getName() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -27,6 +27,8 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.integration.IntegrationPatternType;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.handler.AbstractMessageHandler;
|
||||
import org.springframework.integration.history.MessageHistory;
|
||||
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
|
||||
import org.springframework.integration.support.management.IntegrationManagedResource;
|
||||
import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -193,19 +195,27 @@ public abstract class AbstractMessageRouter extends AbstractMessageHandler imple
|
||||
if (results != null) {
|
||||
int sequenceSize = results.size();
|
||||
int sequenceNumber = 1;
|
||||
Message<?> messageToSend = message;
|
||||
UUID correlationKey = message.getHeaders().getId();
|
||||
boolean hasMessageHistory = message.getHeaders().containsKey(MessageHistory.HEADER_NAME) && sequenceSize > 1;
|
||||
for (MessageChannel channel : results) {
|
||||
final Message<?> messageToSend;
|
||||
if (!this.applySequence) {
|
||||
messageToSend = message;
|
||||
}
|
||||
else {
|
||||
UUID id = message.getHeaders().getId();
|
||||
messageToSend = getMessageBuilderFactory()
|
||||
.fromMessage(message)
|
||||
.pushSequenceDetails(id == null ? generateId() : id,
|
||||
sequenceNumber++, sequenceSize)
|
||||
.build();
|
||||
if (this.applySequence || hasMessageHistory) {
|
||||
AbstractIntegrationMessageBuilder<?> builder =
|
||||
getMessageBuilderFactory()
|
||||
.fromMessage(message);
|
||||
|
||||
if (this.applySequence) {
|
||||
builder.pushSequenceDetails(correlationKey == null ? generateId() : correlationKey,
|
||||
sequenceNumber++, sequenceSize);
|
||||
}
|
||||
|
||||
if (hasMessageHistory) {
|
||||
builder.cloneMessageHistoryIfAny();
|
||||
}
|
||||
|
||||
messageToSend = builder.build();
|
||||
}
|
||||
|
||||
if (channel != null) {
|
||||
sent |= doSend(channel, messageToSend);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -35,6 +35,7 @@ import org.springframework.integration.IntegrationPatternType;
|
||||
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
|
||||
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
|
||||
import org.springframework.integration.handler.DiscardingMessageHandler;
|
||||
import org.springframework.integration.history.MessageHistory;
|
||||
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
|
||||
import org.springframework.integration.support.json.JacksonPresent;
|
||||
import org.springframework.integration.util.FunctionIterator;
|
||||
@@ -276,10 +277,14 @@ public abstract class AbstractMessageSplitter extends AbstractReplyProducingMess
|
||||
Object correlationId, int sequenceNumber, int sequenceSize) {
|
||||
|
||||
AbstractIntegrationMessageBuilder<?> builder = messageBuilderForReply(item);
|
||||
builder.copyHeadersIfAbsent(headers);
|
||||
builder.setHeader(MessageHistory.HEADER_NAME, headers.get(MessageHistory.HEADER_NAME))
|
||||
.copyHeadersIfAbsent(headers)
|
||||
.cloneMessageHistoryIfAny();
|
||||
|
||||
if (this.applySequence) {
|
||||
builder.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2021 the original author or authors.
|
||||
* Copyright 2014-2024 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.
|
||||
@@ -25,6 +25,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.history.MessageHistory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
@@ -165,6 +166,22 @@ public abstract class AbstractIntegrationMessageBuilder<T> {
|
||||
return copyHeadersIfAbsent(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a copy of {@link MessageHistory} header (if present) for a new message to build.
|
||||
* @return the current {@link AbstractIntegrationMessageBuilder}.
|
||||
* @since 6.3
|
||||
*/
|
||||
public AbstractIntegrationMessageBuilder<T> cloneMessageHistoryIfAny() {
|
||||
MessageHistory messageHistory = getHeader(MessageHistory.HEADER_NAME, MessageHistory.class);
|
||||
|
||||
if (messageHistory != null) {
|
||||
return removeHeader(MessageHistory.HEADER_NAME)
|
||||
.setHeader(MessageHistory.HEADER_NAME, messageHistory.clone());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract List<List<Object>> getSequenceDetails();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2022 the original author or authors.
|
||||
* Copyright 2014-2024 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.
|
||||
@@ -186,10 +186,8 @@ public class MessagingAnnotationsWithBeanAnnotationTests {
|
||||
assertThat(messageHistory).isNotNull();
|
||||
String messageHistoryString = messageHistory.toString();
|
||||
assertThat(messageHistoryString)
|
||||
.contains("routerChannel", "filterChannel", "aggregatorChannel", "serviceChannel")
|
||||
.doesNotContain("discardChannel")
|
||||
// history header is not overridden in splitter for individual message from message group emitted before
|
||||
.doesNotContain("splitterChannel");
|
||||
.contains("routerChannel", "filterChannel", "aggregatorChannel", "splitterChannel", "serviceChannel")
|
||||
.doesNotContain("discardChannel");
|
||||
}
|
||||
|
||||
assertThat(this.skippedServiceActivator).isNull();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -96,7 +96,7 @@ public class MessageHistoryTests {
|
||||
Message<Throwable> result2 = MessageHistory.write(result1, new TestComponent(2));
|
||||
assertThat(result2).isInstanceOf(ErrorMessage.class);
|
||||
assertThat(result2).isNotSameAs(original);
|
||||
assertThat(result2).isNotSameAs(result1);
|
||||
assertThat(result2).isSameAs(result1);
|
||||
assertThat(result2.getPayload()).isSameAs(original.getPayload());
|
||||
assertThat(result1).extracting("originalMessage").isSameAs(originalMessage);
|
||||
MessageHistory history2 = MessageHistory.read(result2);
|
||||
@@ -122,20 +122,14 @@ public class MessageHistoryTests {
|
||||
assertThat(result2).isNotSameAs(original);
|
||||
assertThat(result2.getPayload()).isSameAs(original.getPayload());
|
||||
assertThat(((AdviceMessage<?>) result2).getInputMessage()).isSameAs(original.getInputMessage());
|
||||
assertThat(result2).isNotSameAs(result1);
|
||||
assertThat(result2).isSameAs(result1);
|
||||
MessageHistory history2 = MessageHistory.read(result2);
|
||||
assertThat(history2).isNotNull();
|
||||
assertThat(history2.toString()).isEqualTo("testComponent-1,testComponent-2");
|
||||
}
|
||||
|
||||
|
||||
private static class TestComponent implements NamedComponent {
|
||||
|
||||
private final int id;
|
||||
|
||||
TestComponent(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
private record TestComponent(int id) implements NamedComponent {
|
||||
|
||||
@Override
|
||||
public String getComponentName() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -21,8 +21,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
@@ -35,10 +35,12 @@ import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
@@ -52,26 +54,27 @@ public class BroadcastingDispatcherTests {
|
||||
|
||||
private BroadcastingDispatcher dispatcher;
|
||||
|
||||
private final TaskExecutor taskExecutorMock = Mockito.mock(TaskExecutor.class);
|
||||
private final TaskExecutor taskExecutorMock = Mockito.mock();
|
||||
|
||||
private final Message<?> messageMock = Mockito.mock(Message.class);
|
||||
private final Message<?> messageMock = Mockito.mock();
|
||||
|
||||
private final MessageHandler targetMock1 = Mockito.mock(MessageHandler.class);
|
||||
private final MessageHandler targetMock1 = Mockito.mock();
|
||||
|
||||
private final MessageHandler targetMock2 = Mockito.mock(MessageHandler.class);
|
||||
private final MessageHandler targetMock2 = Mockito.mock();
|
||||
|
||||
private final MessageHandler targetMock3 = Mockito.mock(MessageHandler.class);
|
||||
private final MessageHandler targetMock3 = Mockito.mock();
|
||||
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
Mockito.reset(taskExecutorMock, messageMock, taskExecutorMock, targetMock1, targetMock2, targetMock3);
|
||||
given(messageMock.getHeaders()).willReturn(mock());
|
||||
defaultTaskExecutorMock();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void singleTargetWithoutTaskExecutor() throws Exception {
|
||||
public void singleTargetWithoutTaskExecutor() {
|
||||
dispatcher = new BroadcastingDispatcher();
|
||||
dispatcher.addHandler(targetMock1);
|
||||
dispatcher.dispatch(messageMock);
|
||||
@@ -79,7 +82,7 @@ public class BroadcastingDispatcherTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTargetWithTaskExecutor() throws Exception {
|
||||
public void singleTargetWithTaskExecutor() {
|
||||
dispatcher = new BroadcastingDispatcher(taskExecutorMock);
|
||||
dispatcher.addHandler(targetMock1);
|
||||
dispatcher.dispatch(messageMock);
|
||||
@@ -202,12 +205,12 @@ public class BroadcastingDispatcherTests {
|
||||
@Test
|
||||
public void applySequenceDisabledByDefault() {
|
||||
BroadcastingDispatcher dispatcher = new BroadcastingDispatcher();
|
||||
final List<Message<?>> messages = Collections.synchronizedList(new ArrayList<Message<?>>());
|
||||
final List<Message<?>> messages = Collections.synchronizedList(new ArrayList<>());
|
||||
MessageHandler target1 = new MessageStoringTestEndpoint(messages);
|
||||
MessageHandler target2 = new MessageStoringTestEndpoint(messages);
|
||||
dispatcher.addHandler(target1);
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.dispatch(new GenericMessage<String>("test"));
|
||||
dispatcher.dispatch(new GenericMessage<>("test"));
|
||||
assertThat(messages.size()).isEqualTo(2);
|
||||
assertThat(new IntegrationMessageHeaderAccessor(messages.get(0)).getSequenceNumber()).isEqualTo(0);
|
||||
assertThat(new IntegrationMessageHeaderAccessor(messages.get(0)).getSequenceSize()).isEqualTo(0);
|
||||
@@ -219,14 +222,14 @@ public class BroadcastingDispatcherTests {
|
||||
public void applySequenceEnabled() {
|
||||
BroadcastingDispatcher dispatcher = new BroadcastingDispatcher();
|
||||
dispatcher.setApplySequence(true);
|
||||
final List<Message<?>> messages = Collections.synchronizedList(new ArrayList<Message<?>>());
|
||||
final List<Message<?>> messages = Collections.synchronizedList(new ArrayList<>());
|
||||
MessageHandler target1 = new MessageStoringTestEndpoint(messages);
|
||||
MessageHandler target2 = new MessageStoringTestEndpoint(messages);
|
||||
MessageHandler target3 = new MessageStoringTestEndpoint(messages);
|
||||
dispatcher.addHandler(target1);
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.addHandler(target3);
|
||||
Message<?> inputMessage = new GenericMessage<String>("test");
|
||||
Message<?> inputMessage = new GenericMessage<>("test");
|
||||
Object originalId = inputMessage.getHeaders().getId();
|
||||
dispatcher.dispatch(inputMessage);
|
||||
assertThat(messages.size()).isEqualTo(3);
|
||||
@@ -251,13 +254,11 @@ public class BroadcastingDispatcherTests {
|
||||
dispatcher.addHandler(targetMock1);
|
||||
doThrow(new MessagingException("Mock Exception"))
|
||||
.when(targetMock1).handleMessage(eq(messageMock));
|
||||
try {
|
||||
dispatcher.dispatch(messageMock);
|
||||
fail("Expected Exception");
|
||||
}
|
||||
catch (MessagingException e) {
|
||||
assertThat(e.getFailedMessage()).isEqualTo(messageMock);
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(MessagingException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(messageMock))
|
||||
.extracting(MessagingException::getFailedMessage)
|
||||
.isEqualTo(messageMock);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,13 +273,11 @@ public class BroadcastingDispatcherTests {
|
||||
Message<String> dontReplaceThisMessage = MessageBuilder.withPayload("x").build();
|
||||
doThrow(new MessagingException(dontReplaceThisMessage, "Mock Exception"))
|
||||
.when(targetMock1).handleMessage(eq(messageMock));
|
||||
try {
|
||||
dispatcher.dispatch(messageMock);
|
||||
fail("Expected Exception");
|
||||
}
|
||||
catch (MessagingException e) {
|
||||
assertThat(e.getFailedMessage()).isEqualTo(dontReplaceThisMessage);
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(MessagingException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(messageMock))
|
||||
.extracting(MessagingException::getFailedMessage)
|
||||
.isEqualTo(dontReplaceThisMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,13 +305,10 @@ public class BroadcastingDispatcherTests {
|
||||
@Test
|
||||
public void testNoHandlerWithRequiredSubscriber() {
|
||||
dispatcher = new BroadcastingDispatcher(true);
|
||||
try {
|
||||
dispatcher.dispatch(messageMock);
|
||||
fail("Expected Exception");
|
||||
}
|
||||
catch (MessageDispatchingException exception) {
|
||||
assertThat(exception.getFailedMessage()).isEqualTo(messageMock);
|
||||
}
|
||||
assertThatExceptionOfType(MessageDispatchingException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(messageMock))
|
||||
.extracting(MessagingException::getFailedMessage)
|
||||
.isEqualTo(messageMock);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,13 +318,10 @@ public class BroadcastingDispatcherTests {
|
||||
@Test
|
||||
public void testNoHandlerWithExecutorWithRequiredSubscriber() {
|
||||
dispatcher = new BroadcastingDispatcher(taskExecutorMock, true);
|
||||
try {
|
||||
dispatcher.dispatch(messageMock);
|
||||
fail("Expected Exception");
|
||||
}
|
||||
catch (MessageDispatchingException exception) {
|
||||
assertThat(exception.getFailedMessage()).isEqualTo(messageMock);
|
||||
}
|
||||
assertThatExceptionOfType(MessageDispatchingException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(messageMock))
|
||||
.extracting(MessagingException::getFailedMessage)
|
||||
.isEqualTo(messageMock);
|
||||
}
|
||||
|
||||
private void defaultTaskExecutorMock() {
|
||||
|
||||
@@ -129,7 +129,9 @@ The MBean's object name is `<domain>:name=messageHistoryConfigurer,type=MessageH
|
||||
IMPORTANT: Only one `@EnableMessageHistory` (or `<message-history/>`) must be declared in the application context as single source for components tracking configuration.
|
||||
Do not use a generic bean definition for the `MessageHistoryConfigurer`.
|
||||
|
||||
NOTE: By definition, the message history header is immutable (you cannot re-write history).
|
||||
Therefore, when writing message history values, the components either create new messages (when the component is an origin) or they copy the history from a request message, modifying it and setting the new list on a reply message.
|
||||
In either case, the values can be appended even if the message itself is crossing thread boundaries.
|
||||
That means that the history values can greatly simplify debugging in an asynchronous message flow.
|
||||
NOTE: Prior to version 6.3, the message history header was immutable (you cannot re-write history): every single track created not only new instance of the `MessageHistory`, but a fully new message copy.
|
||||
Now it works in append-only mode: the first track creates a new message with a new `MessageHistory` container.
|
||||
All the rest `MessageHistory.write()` calls add new entries to existing header - and no new message created.
|
||||
This significantly improves the application performance.
|
||||
All the components in the framework, where same message can be sent to several consumers (`PublishSubscribeChannel`, `AbstractMessageRouter`, `WireTap` etc.), or splitter produces several outputs based on the input message, are now cloning an existing `MessageHistory` header into those new messages.
|
||||
For any other multi-producing use-cases, outside the framework scope, the `AbstractIntegrationMessageBuilder.cloneMessageHistoryIfAny()` API is recommended to ensure that parallel downstream sub-flows contribute their own message history traces.
|
||||
@@ -19,6 +19,10 @@ In general the project has been moved to the latest dependency versions.
|
||||
[[x6.3-general]]
|
||||
=== General Changes
|
||||
|
||||
The `MessageHistory` header is now mutable, append-only container.
|
||||
And all the subsequent tracks don't create new message - only their entry is added to existing message history header.
|
||||
See xref:message-history.adoc[Message History Chapter] for more information.
|
||||
|
||||
[[x6.3-security-changes]]
|
||||
=== Security Support Changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user