GH-9416: Extract BaseMessageBuilder for easier message extensions

Fixes: #9416
Issue link: https://github.com/spring-projects/spring-integration/issues/9416

The `MessageBuilderFactory` bean could be used a central place to provide custom `Message`
implementation into the application.
For example, the `GenericMessage.toString()` can be overridden to remove or mask sensitive
information from the payload or headers.

* Extract a `BaseMessageBuilder` from the `MessageBuilder` class to simplify
a custom `MessageBuilderFactory` implementation
* Test and document new feature and its purpose
This commit is contained in:
Artem Bilan
2024-10-29 15:40:39 -04:00
parent 4ee55326df
commit f87aff3aa8
6 changed files with 497 additions and 285 deletions

View File

@@ -108,7 +108,7 @@ public class IntegrationMessageHeaderAccessor extends MessageHeaderAccessor {
* @see #isReadOnly(String)
*/
public void setReadOnlyHeaders(String... readOnlyHeaders) {
Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not be contain null items.");
Assert.noNullElements(readOnlyHeaders, "'readOnlyHeaders' must not contain null items.");
if (!ObjectUtils.isEmpty(readOnlyHeaders)) {
this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders));
}

View File

@@ -0,0 +1,335 @@
/*
* Copyright 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.
* 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.integration.support;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* The {@link AbstractIntegrationMessageBuilder} extension for the default logic to build message.
* The {@link MessageBuilder} is fully based on this class.
* This abstract class can be used for creating custom {@link Message} instances.
* For that purpose its {@link #build()} method has to be overridden.
* The custom {@link Message} type could be used, for example, to hide sensitive information
* from payload and headers when message is logged.
* For this goal there would be enough to override {@link GenericMessage#toString()}
* and filter out (or mask) those headers which container such sensitive information.
*
* @param <T> the payload type.
* @param <B> the target builder class type.
*
* @author Artem Bilan
*
* @since 6.4
*
* @see MessageBuilder
* @see MessageBuilderFactory
*/
public abstract class BaseMessageBuilder<T, B extends BaseMessageBuilder<T, B>>
extends AbstractIntegrationMessageBuilder<T> {
private static final Log LOGGER = LogFactory.getLog(BaseMessageBuilder.class);
private final T payload;
private final IntegrationMessageHeaderAccessor headerAccessor;
@Nullable
private final Message<T> originalMessage;
private volatile boolean modified;
private String[] readOnlyHeaders;
protected BaseMessageBuilder(T payload, @Nullable Message<T> originalMessage) {
Assert.notNull(payload, "payload must not be null");
this.payload = payload;
this.originalMessage = originalMessage;
this.headerAccessor = new IntegrationMessageHeaderAccessor(originalMessage);
if (originalMessage != null) {
this.modified = (!this.payload.equals(originalMessage.getPayload()));
}
}
@Override
public T getPayload() {
return this.payload;
}
@Override
public Map<String, Object> getHeaders() {
return this.headerAccessor.toMap();
}
@Nullable
@Override
public <V> V getHeader(String key, Class<V> type) {
return this.headerAccessor.getHeader(key, type);
}
/**
* Set the value for the given header name. If the provided value is {@code null}, the header will be removed.
* @param headerName The header name.
* @param headerValue The header value.
* @return this MessageBuilder.
*/
@Override
public B setHeader(String headerName, @Nullable Object headerValue) {
this.headerAccessor.setHeader(headerName, headerValue);
return _this();
}
/**
* Set the value for the given header name only if the header name is not already associated with a value.
* @param headerName The header name.
* @param headerValue The header value.
* @return this MessageBuilder.
*/
@Override
public B setHeaderIfAbsent(String headerName, Object headerValue) {
this.headerAccessor.setHeaderIfAbsent(headerName, headerValue);
return _this();
}
/**
* Removes all headers provided via array of 'headerPatterns'. As the name suggests the array
* may contain simple matching patterns for header names. Supported pattern styles are:
* {@code xxx*}, {@code *xxx}, {@code *xxx*} and {@code xxx*yyy}.
* @param headerPatterns The header patterns.
* @return this MessageBuilder.
*/
@Override
public B removeHeaders(String... headerPatterns) {
this.headerAccessor.removeHeaders(headerPatterns);
return _this();
}
/**
* Remove the value for the given header name.
* @param headerName The header name.
* @return this MessageBuilder.
*/
@Override
public B removeHeader(String headerName) {
if (!this.headerAccessor.isReadOnly(headerName)) {
this.headerAccessor.removeHeader(headerName);
}
else if (LOGGER.isInfoEnabled()) {
LOGGER.info("The header [" + headerName + "] is ignored for removal because it is is readOnly.");
}
return _this();
}
/**
* Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use
* {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values
* will never be overwritten.
* @param headersToCopy The headers to copy.
* @return this MessageBuilder.
* @see MessageHeaders#ID
* @see MessageHeaders#TIMESTAMP
*/
@Override
public B copyHeaders(@Nullable Map<String, ?> headersToCopy) {
this.headerAccessor.copyHeaders(headersToCopy);
return _this();
}
/**
* Copy the name-value pairs from the provided Map. This operation will not override any existing values.
* @param headersToCopy The headers to copy.
* @return this MessageBuilder.
*/
@Override
public B copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy != null) {
for (Map.Entry<String, ?> entry : headersToCopy.entrySet()) {
String headerName = entry.getKey();
if (!this.headerAccessor.isReadOnly(headerName)) {
this.headerAccessor.setHeaderIfAbsent(headerName, entry.getValue());
}
}
}
return _this();
}
@SuppressWarnings("unchecked")
@Override
@Nullable
protected List<List<Object>> getSequenceDetails() {
return (List<List<Object>>) this.headerAccessor.getHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
}
@Override
@Nullable
protected Object getCorrelationId() {
return this.headerAccessor.getCorrelationId();
}
@Override
protected Object getSequenceNumber() {
return this.headerAccessor.getSequenceNumber();
}
@Override
protected Object getSequenceSize() {
return this.headerAccessor.getSequenceSize();
}
@Override
public B pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) {
super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize);
return _this();
}
@Override
public B popSequenceDetails() {
super.popSequenceDetails();
return _this();
}
@Override
public B setExpirationDate(@Nullable Long expirationDate) {
super.setExpirationDate(expirationDate);
return _this();
}
@Override
public B setExpirationDate(@Nullable Date expirationDate) {
super.setExpirationDate(expirationDate);
return _this();
}
@Override
public B setCorrelationId(Object correlationId) {
super.setCorrelationId(correlationId);
return _this();
}
@Override
public B setReplyChannel(MessageChannel replyChannel) {
super.setReplyChannel(replyChannel);
return _this();
}
@Override
public B setReplyChannelName(String replyChannelName) {
super.setReplyChannelName(replyChannelName);
return _this();
}
@Override
public B setErrorChannel(MessageChannel errorChannel) {
super.setErrorChannel(errorChannel);
return _this();
}
@Override
public B setErrorChannelName(String errorChannelName) {
super.setErrorChannelName(errorChannelName);
return _this();
}
@Override
public B setSequenceNumber(Integer sequenceNumber) {
super.setSequenceNumber(sequenceNumber);
return _this();
}
@Override
public B setSequenceSize(Integer sequenceSize) {
super.setSequenceSize(sequenceSize);
return _this();
}
@Override
public B setPriority(Integer priority) {
super.setPriority(priority);
return _this();
}
/**
* Specify a list of headers which should be considered as read only
* and prohibited from being populated in the message.
* @param readOnlyHeaders the list of headers for {@code readOnly} mode.
* Defaults to {@link MessageHeaders#ID} and {@link MessageHeaders#TIMESTAMP}.
* @return the current {@link BaseMessageBuilder}
* @see IntegrationMessageHeaderAccessor#isReadOnly(String)
*/
public B readOnlyHeaders(@Nullable String... readOnlyHeaders) {
this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null;
if (readOnlyHeaders != null) {
this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders);
}
return _this();
}
/**
* Return an original message instance if it is not modified and does not have read-only headers.
* If payload is an instance of {@link Throwable}, then an {@link ErrorMessage} is built.
* Otherwise, a new instance of {@link GenericMessage} is produced.
* This method can be overridden to provide any custom message implementations.
* @return the message instance
* @see #getPayload()
* @see #getHeaders()
*/
@Override
@SuppressWarnings("unchecked")
public Message<T> build() {
if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null
&& !containsReadOnly(this.originalMessage.getHeaders())) {
return this.originalMessage;
}
if (payload instanceof Throwable throwable) {
return (Message<T>) new ErrorMessage(throwable, getHeaders());
}
return new GenericMessage<>(payload, getHeaders());
}
private boolean containsReadOnly(MessageHeaders headers) {
if (!ObjectUtils.isEmpty(this.readOnlyHeaders)) {
for (String readOnly : this.readOnlyHeaders) {
if (headers.containsKey(readOnly)) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
private B _this() {
return (B) this;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 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,23 +16,10 @@
package org.springframework.integration.support;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* The default message builder; creates immutable {@link GenericMessage}s.
@@ -48,52 +35,17 @@ import org.springframework.util.ObjectUtils;
* @author Gary Russell
* @author Artem Bilan
*/
public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T> {
private static final Log LOGGER = LogFactory.getLog(MessageBuilder.class);
private final T payload;
private final IntegrationMessageHeaderAccessor headerAccessor;
@Nullable
private final Message<T> originalMessage;
private volatile boolean modified;
private String[] readOnlyHeaders;
public final class MessageBuilder<T> extends BaseMessageBuilder<T, MessageBuilder<T>> {
/**
* Private constructor to be invoked from the static factory methods only.
*/
private MessageBuilder(T payload, @Nullable Message<T> originalMessage) {
Assert.notNull(payload, "payload must not be null");
this.payload = payload;
this.originalMessage = originalMessage;
this.headerAccessor = new IntegrationMessageHeaderAccessor(originalMessage);
if (originalMessage != null) {
this.modified = (!this.payload.equals(originalMessage.getPayload()));
}
}
@Override
public T getPayload() {
return this.payload;
}
@Override
public Map<String, Object> getHeaders() {
return this.headerAccessor.toMap();
}
@Nullable
@Override
public <V> V getHeader(String key, Class<V> type) {
return this.headerAccessor.getHeader(key, type);
super(payload, originalMessage);
}
/**
* Create a builder for a new {@link Message} instance pre-populated with all of the headers copied from the
* Create a builder for a new {@link Message} instance pre-populated with all the headers copied from the
* provided message. The payload of the provided Message will also be used as the payload for the new message.
* @param message the Message from which the payload and all headers will be copied
* @param <T> The type of the payload.
@@ -114,230 +66,4 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
return new MessageBuilder<>(payload, null);
}
/**
* Set the value for the given header name. If the provided value is <code>null</code>, the header will be removed.
* @param headerName The header name.
* @param headerValue The header value.
* @return this MessageBuilder.
*/
@Override
public MessageBuilder<T> setHeader(String headerName, @Nullable Object headerValue) {
this.headerAccessor.setHeader(headerName, headerValue);
return this;
}
/**
* Set the value for the given header name only if the header name is not already associated with a value.
* @param headerName The header name.
* @param headerValue The header value.
* @return this MessageBuilder.
*/
@Override
public MessageBuilder<T> setHeaderIfAbsent(String headerName, Object headerValue) {
this.headerAccessor.setHeaderIfAbsent(headerName, headerValue);
return this;
}
/**
* Removes all headers provided via array of 'headerPatterns'. As the name suggests the array
* may contain simple matching patterns for header names. Supported pattern styles are:
* "xxx*", "*xxx", "*xxx*" and "xxx*yyy".
* @param headerPatterns The header patterns.
* @return this MessageBuilder.
*/
@Override
public MessageBuilder<T> removeHeaders(String... headerPatterns) {
this.headerAccessor.removeHeaders(headerPatterns);
return this;
}
/**
* Remove the value for the given header name.
* @param headerName The header name.
* @return this MessageBuilder.
*/
@Override
public MessageBuilder<T> removeHeader(String headerName) {
if (!this.headerAccessor.isReadOnly(headerName)) {
this.headerAccessor.removeHeader(headerName);
}
else if (LOGGER.isInfoEnabled()) {
LOGGER.info("The header [" + headerName + "] is ignored for removal because it is is readOnly.");
}
return this;
}
/**
* Copy the name-value pairs from the provided Map. This operation will overwrite any existing values. Use {
* {@link #copyHeadersIfAbsent(Map)} to avoid overwriting values. Note that the 'id' and 'timestamp' header values
* will never be overwritten.
* @param headersToCopy The headers to copy.
* @return this MessageBuilder.
* @see MessageHeaders#ID
* @see MessageHeaders#TIMESTAMP
*/
@Override
public MessageBuilder<T> copyHeaders(@Nullable Map<String, ?> headersToCopy) {
this.headerAccessor.copyHeaders(headersToCopy);
return this;
}
/**
* Copy the name-value pairs from the provided Map. This operation will <em>not</em> overwrite any existing values.
* @param headersToCopy The headers to copy.
* @return this MessageBuilder.
*/
@Override
public MessageBuilder<T> copyHeadersIfAbsent(@Nullable Map<String, ?> headersToCopy) {
if (headersToCopy != null) {
for (Map.Entry<String, ?> entry : headersToCopy.entrySet()) {
String headerName = entry.getKey();
if (!this.headerAccessor.isReadOnly(headerName)) {
this.headerAccessor.setHeaderIfAbsent(headerName, entry.getValue());
}
}
}
return this;
}
@SuppressWarnings("unchecked")
@Override
@Nullable
protected List<List<Object>> getSequenceDetails() {
return (List<List<Object>>) this.headerAccessor.getHeader(IntegrationMessageHeaderAccessor.SEQUENCE_DETAILS);
}
@Override
@Nullable
protected Object getCorrelationId() {
return this.headerAccessor.getCorrelationId();
}
@Override
protected Object getSequenceNumber() {
return this.headerAccessor.getSequenceNumber();
}
@Override
protected Object getSequenceSize() {
return this.headerAccessor.getSequenceSize();
}
/*
* The following overrides (delegating to super) are provided to ease the
* pain for existing applications that use the builder API and expect
* a MessageBuilder to be returned.
*/
@Override
public MessageBuilder<T> pushSequenceDetails(Object correlationId, int sequenceNumber, int sequenceSize) {
super.pushSequenceDetails(correlationId, sequenceNumber, sequenceSize);
return this;
}
@Override
public MessageBuilder<T> popSequenceDetails() {
super.popSequenceDetails();
return this;
}
@Override
public MessageBuilder<T> setExpirationDate(@Nullable Long expirationDate) {
super.setExpirationDate(expirationDate);
return this;
}
@Override
public MessageBuilder<T> setExpirationDate(@Nullable Date expirationDate) {
super.setExpirationDate(expirationDate);
return this;
}
@Override
public MessageBuilder<T> setCorrelationId(Object correlationId) {
super.setCorrelationId(correlationId);
return this;
}
@Override
public MessageBuilder<T> setReplyChannel(MessageChannel replyChannel) {
super.setReplyChannel(replyChannel);
return this;
}
@Override
public MessageBuilder<T> setReplyChannelName(String replyChannelName) {
super.setReplyChannelName(replyChannelName);
return this;
}
@Override
public MessageBuilder<T> setErrorChannel(MessageChannel errorChannel) {
super.setErrorChannel(errorChannel);
return this;
}
@Override
public MessageBuilder<T> setErrorChannelName(String errorChannelName) {
super.setErrorChannelName(errorChannelName);
return this;
}
@Override
public MessageBuilder<T> setSequenceNumber(Integer sequenceNumber) {
super.setSequenceNumber(sequenceNumber);
return this;
}
@Override
public MessageBuilder<T> setSequenceSize(Integer sequenceSize) {
super.setSequenceSize(sequenceSize);
return this;
}
@Override
public MessageBuilder<T> setPriority(Integer priority) {
super.setPriority(priority);
return this;
}
/**
* Specify a list of headers which should be considered as read only
* and prohibited from being populated in the message.
* @param readOnlyHeaders the list of headers for {@code readOnly} mode.
* Defaults to {@link MessageHeaders#ID} and {@link MessageHeaders#TIMESTAMP}.
* @return the current {@link MessageBuilder}
* @since 4.3.2
* @see IntegrationMessageHeaderAccessor#isReadOnly(String)
*/
public MessageBuilder<T> readOnlyHeaders(String... readOnlyHeaders) {
this.readOnlyHeaders = readOnlyHeaders != null ? Arrays.copyOf(readOnlyHeaders, readOnlyHeaders.length) : null;
this.headerAccessor.setReadOnlyHeaders(readOnlyHeaders);
return this;
}
@Override
@SuppressWarnings("unchecked")
public Message<T> build() {
if (!this.modified && !this.headerAccessor.isModified() && this.originalMessage != null
&& !containsReadOnly(this.originalMessage.getHeaders())) {
return this.originalMessage;
}
if (this.payload instanceof Throwable) {
return (Message<T>) new ErrorMessage((Throwable) this.payload, this.headerAccessor.toMap());
}
return new GenericMessage<>(this.payload, this.headerAccessor.toMap());
}
private boolean containsReadOnly(MessageHeaders headers) {
if (!ObjectUtils.isEmpty(this.readOnlyHeaders)) {
for (String readOnly : this.readOnlyHeaders) {
if (headers.containsKey(readOnly)) {
return true;
}
}
}
return false;
}
}

View File

@@ -16,16 +16,22 @@
package org.springframework.integration.support;
import org.junit.Test;
import java.io.Serial;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Gary Russell
* @author Artem Bilan
* @since 4.3.10
*
*/
public class MessageBuilderTests {
@@ -45,4 +51,71 @@ public class MessageBuilderTests {
assertThat(message.getHeaders().get("qux")).isNull();
}
@Test
public void personalInfoHeadersAreMaskedWithCustomMessage() {
Message<String> message =
MessageBuilder.withPayload("some_user")
.setHeader("password", "some_password")
.build();
Message<String> piiMessage = new PiiMessageBuilderFactory().fromMessage(message).build();
assertThat(piiMessage).isInstanceOf(PiiMessage.class);
assertThat(piiMessage.getPayload()).isEqualTo("some_user");
assertThat(piiMessage.getHeaders().get("password")).isEqualTo("some_password");
assertThat(piiMessage.toString())
.doesNotContain("some_password")
.contains("******");
}
private static class PiiMessageBuilderFactory implements MessageBuilderFactory {
@Override
public <T> PiiMessageBuilder<T> fromMessage(Message<T> message) {
return new PiiMessageBuilder<>(message.getPayload(), message);
}
@Override
public <T> PiiMessageBuilder<T> withPayload(T payload) {
return new PiiMessageBuilder<>(payload, null);
}
}
private static class PiiMessageBuilder<P> extends BaseMessageBuilder<P, PiiMessageBuilder<P>> {
public PiiMessageBuilder(P payload, @Nullable Message<P> originalMessage) {
super(payload, originalMessage);
}
@Override
public Message<P> build() {
return new PiiMessage<>(getPayload(), getHeaders());
}
}
private static class PiiMessage<P> extends GenericMessage<P> {
@Serial
private static final long serialVersionUID = -354503673433669578L;
public PiiMessage(P payload, Map<String, Object> headers) {
super(payload, headers);
}
@Override
public String toString() {
return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']';
}
private static Map<String, Object> maskHeaders(Map<String, Object> headers) {
return headers.entrySet()
.stream()
.map((entry) -> entry.getKey().equals("password") ? Map.entry(entry.getKey(), "******") : entry)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
}

View File

@@ -245,14 +245,14 @@ When messages are processed (and modified) by message-producing endpoints (such
One exception to this is a xref:transformer.adoc[transformer], when a complete message is returned to the framework.
In that case, the user code is responsible for the entire outbound message.
When a transformer just returns the payload, the inbound headers are propagated.
Also, a header is only propagated if it does not already exist in the outbound message, letting you change header values as needed.
Also, a header only propagated if it does not already exist in the outbound message, letting you change header values as needed.
Starting with version 4.3.10, you can configure message handlers (that modify messages and produce output) to suppress the propagation of specific headers.
To configure the header(s) you do not want to be copied, call the `setNotPropagatedHeaders()` or `addNotPropagatedHeaders()` methods on the `MessageProducingMessageHandler` abstract class.
You can also globally suppress propagation of specific message headers by setting the `readOnlyHeaders` property in `META-INF/spring.integration.properties` to a comma-delimited list of headers.
Starting with version 5.0, the `setNotPropagatedHeaders()` implementation on the `AbstractMessageProducingHandler` applies simple patterns (`xxx*`, `*xxx`, `*xxx*`, or `xxx*yyy`) to allow filtering headers with a common suffix or prefix.
Starting with version 5.0, the `setNotPropagatedHeaders()` implementation on the `AbstractMessageProducingHandler` applies simple patterns (`xxx*`, `\*xxx`, `*xxx*`, or `xxx*yyy`) to allow filtering headers with a common suffix or prefix.
See https://docs.spring.io/spring-integration/api/org/springframework/integration/util/PatternMatchUtils.html[`PatternMatchUtils` Javadoc] for more information.
When one of the patterns is `*` (asterisk), no headers are propagated.
All other patterns are ignored.
@@ -290,13 +290,15 @@ Throwable t = message.getPayload();
Note that this implementation takes advantage of the fact that the `GenericMessage` base class is parameterized.
Therefore, as shown in both examples, no casting is necessary when retrieving the `Message` payload `Object`.
The mentioned `Message` class implementations are immutable.
In some cases, when mutability is not a concern and the logic of application is well-designed to avoid concurrent modifications, a `MutableMessage` can be used.
[[message-builder]]
== The `MessageBuilder` Helper Class
You may notice that the `Message` interface defines retrieval methods for its payload and headers but provides no setters.
The reason for this is that a `Message` cannot be modified after its initial creation.
Therefore, when a `Message` instance is sent to multiple consumers (for example,
through a publish-subscribe Channel), if one of those consumers needs to send a reply with a different payload type, it must create a new `Message`.
Therefore, when a `Message` instance is sent to multiple consumers (for example, through a publish-subscribe Channel), if one of those consumers needs to send a reply with a different payload type, it must create a new `Message`.
As a result, the other consumers are not affected by those changes.
Keep in mind that multiple consumers may access the same payload instance or header value, and whether such an instance is itself immutable is a decision left to you.
In other words, the contract for `Message` instances is similar to that of an unmodifiable `Collection`, and the `MessageHeaders` map further exemplifies that.
@@ -359,3 +361,76 @@ assertEquals(2, lessImportantMessage.getHeaders().getPriority());
The `priority` header is considered only when using a `PriorityChannel` (as described in the next chapter).
It is defined as a `java.lang.Integer`.
The `MutableMessageBuilder` is provided to deal with `MutableMessage` instances.
The logic of this class is to create a `MutableMessage` or leave it as is and mutate its content via builder methods.
This way there is a slight performance gain in the running application, when immutability is not a concern of message exchanges.
NOTE: Starting with version 6.4, a `BaseMessageBuilder` class is extracted from the `MessageBuilder` to simplify an extension for the default message building logic.
For example, together with a custom `MessageBuilderFactory`, a custom `BaseMessageBuilder` implementation could be used globally in the application context to provide custom `Message` instances.
In particular, the `GenericMessage.toString()` method can be overridden to hide sensitive information from payload and headers when such a message is logged.
[[message-builder-factory]]
== The `MessageBuilderFactory` abstraction
The `MessageBuilderFactory` bean with `IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME` is registered globally into an application context and used everywhere in the framework to create `Message` instances.
By default, it is an instance of `DefaultMessageBuilderFactory`.
Out of the box, the framework also provides a `MutableMessageBuilderFactory` to create `MutableMessage` instances in the framework components instead.
To customize `Message` instances creation, a `MessageBuilderFactory` bean with `IntegrationUtils.INTEGRATION_MESSAGE_BUILDER_FACTORY_BEAN_NAME` has to be provided in the target application context to override a default one.
For example, a custom `MessageBuilderFactory` could be registered for an implementation of the `BaseMessageBuilder` where we would like to provide a `GenericMessage` extension with overridden `toString()` to to hide sensitive information from payload and headers when such a message is logged.
Some quick implementation of these classes to demonstrate a personal identifiable information mitigation can be like this:
[source,java]
----
class PiiMessageBuilderFactory implements MessageBuilderFactory {
@Override
public <T> PiiMessageBuilder<T> fromMessage(Message<T> message) {
return new PiiMessageBuilder<>(message.getPayload(), message);
}
@Override
public <T> PiiMessageBuilder<T> withPayload(T payload) {
return new PiiMessageBuilder<>(payload, null);
}
}
class PiiMessageBuilder<P> extends BaseMessageBuilder<P, PiiMessageBuilder<P>> {
public PiiMessageBuilder(P payload, @Nullable Message<P> originalMessage) {
super(payload, originalMessage);
}
@Override
public Message<P> build() {
return new PiiMessage<>(getPayload(), getHeaders());
}
}
class PiiMessage<P> extends GenericMessage<P> {
@Serial
private static final long serialVersionUID = -354503673433669578L;
public PiiMessage(P payload, Map<String, Object> headers) {
super(payload, headers);
}
@Override
public String toString() {
return "PiiMessage [payload=" + getPayload() + ", headers=" + maskHeaders(getHeaders()) + ']';
}
private static Map<String, Object> maskHeaders(Map<String, Object> headers) {
return headers.entrySet()
.stream()
.map((entry) -> entry.getKey().equals("password") ? Map.entry(entry.getKey(), "******") : entry)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
----
The this `PiiMessageBuilderFactory` could be registered as a bean, and whenever the framework logs the message (e.g. in case of `errorChannel`), the `password` header will be masked.

View File

@@ -16,6 +16,9 @@ In general the project has been moved to the latest dependency versions.
[[x6.4-new-components]]
=== New Components
A `BaseMessageBuilder` class has been extracted from the `MessageBuilder` to simplify a custom builder implementation where the most of the logic should be the same as `MessageBuilder` one.
See xref:message.adoc#message-builder[`MessageBuilder`] for more information.
The new Control Bus interaction model is implemented in the `ControlBusCommandRegistry`.
A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`.
See xref:control-bus.adoc[Control Bus] for more information.