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:
@@ -108,7 +108,7 @@ public class IntegrationMessageHeaderAccessor extends MessageHeaderAccessor {
|
|||||||
* @see #isReadOnly(String)
|
* @see #isReadOnly(String)
|
||||||
*/
|
*/
|
||||||
public void setReadOnlyHeaders(String... readOnlyHeaders) {
|
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)) {
|
if (!ObjectUtils.isEmpty(readOnlyHeaders)) {
|
||||||
this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders));
|
this.readOnlyHeaders = new HashSet<>(Arrays.asList(readOnlyHeaders));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,23 +16,10 @@
|
|||||||
|
|
||||||
package org.springframework.integration.support;
|
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.lang.Nullable;
|
||||||
import org.springframework.messaging.Message;
|
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.messaging.support.GenericMessage;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default message builder; creates immutable {@link GenericMessage}s.
|
* The default message builder; creates immutable {@link GenericMessage}s.
|
||||||
@@ -48,52 +35,17 @@ import org.springframework.util.ObjectUtils;
|
|||||||
* @author Gary Russell
|
* @author Gary Russell
|
||||||
* @author Artem Bilan
|
* @author Artem Bilan
|
||||||
*/
|
*/
|
||||||
public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T> {
|
public final class MessageBuilder<T> extends BaseMessageBuilder<T, MessageBuilder<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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private constructor to be invoked from the static factory methods only.
|
* Private constructor to be invoked from the static factory methods only.
|
||||||
*/
|
*/
|
||||||
private MessageBuilder(T payload, @Nullable Message<T> originalMessage) {
|
private MessageBuilder(T payload, @Nullable Message<T> originalMessage) {
|
||||||
Assert.notNull(payload, "payload must not be null");
|
super(payload, originalMessage);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* 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 message the Message from which the payload and all headers will be copied
|
||||||
* @param <T> The type of the payload.
|
* @param <T> The type of the payload.
|
||||||
@@ -114,230 +66,4 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
|
|||||||
return new MessageBuilder<>(payload, null);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,22 @@
|
|||||||
|
|
||||||
package org.springframework.integration.support;
|
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.Message;
|
||||||
|
import org.springframework.messaging.support.GenericMessage;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Gary Russell
|
* @author Gary Russell
|
||||||
|
* @author Artem Bilan
|
||||||
* @since 4.3.10
|
* @since 4.3.10
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class MessageBuilderTests {
|
public class MessageBuilderTests {
|
||||||
|
|
||||||
@@ -45,4 +51,71 @@ public class MessageBuilderTests {
|
|||||||
assertThat(message.getHeaders().get("qux")).isNull();
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
When one of the patterns is `*` (asterisk), no headers are propagated.
|
||||||
All other patterns are ignored.
|
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.
|
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`.
|
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]]
|
[[message-builder]]
|
||||||
== The `MessageBuilder` Helper Class
|
== The `MessageBuilder` Helper Class
|
||||||
|
|
||||||
You may notice that the `Message` interface defines retrieval methods for its payload and headers but provides no setters.
|
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.
|
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,
|
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`.
|
||||||
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.
|
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.
|
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.
|
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).
|
The `priority` header is considered only when using a `PriorityChannel` (as described in the next chapter).
|
||||||
It is defined as a `java.lang.Integer`.
|
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.
|
||||||
@@ -16,6 +16,9 @@ In general the project has been moved to the latest dependency versions.
|
|||||||
[[x6.4-new-components]]
|
[[x6.4-new-components]]
|
||||||
=== 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`.
|
The new Control Bus interaction model is implemented in the `ControlBusCommandRegistry`.
|
||||||
A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`.
|
A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`.
|
||||||
See xref:control-bus.adoc[Control Bus] for more information.
|
See xref:control-bus.adoc[Control Bus] for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user