GH-9381: Introduce Control Bus commands management

Fixes: #9381

Currently, there is no way to know in one place what Control Bus commands are available and with what arguments

* Add `ControlBusCommandRegistry` infrastructure bean to gather control bus commands from beans and expose them for invocation
* Add `ControlBusController` to expose a `/control-bus` REST service against the mentioned `ControlBusCommandRegistry`
* Add `@EnableIntegrationManagement(loadControlBusCommands)` to be able to load all the Control Bus commands from the application context instead of on demand by default
* Deprecated existing SpEL(and Groovy)-based Control Bus functionality in favor of new, more manageable, logic
This commit is contained in:
Artem Bilan
2024-08-08 11:07:56 -04:00
parent 77e3b08d16
commit 4d787554b8
81 changed files with 1826 additions and 855 deletions

View File

@@ -80,10 +80,15 @@ public class IntegrationMessageHeaderAccessor extends MessageHeaderAccessor {
public static final String SOURCE_DATA = "sourceData";
/**
* Raw source message.
* The header for {@link reactor.util.context.ContextView}.
*/
public static final String REACTOR_CONTEXT = "reactorContext";
/**
* The header for Control Bus command arguments. Must be a list of values.
*/
public static final String CONTROL_BUS_ARGUMENTS = "controlBusArguments";
private static final BiFunction<String, String, String> TYPE_VERIFY_MESSAGE_FUNCTION =
(name, trailer) -> "The '" + name + trailer;

View File

@@ -0,0 +1,47 @@
/*
* 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.config;
import org.springframework.integration.handler.ControlBusMessageProcessor;
import org.springframework.integration.handler.ServiceActivatingHandler;
import org.springframework.messaging.MessageHandler;
/**
* FactoryBean for creating {@link MessageHandler} instances to handle a message with a Control Bus command.
*
* @author Artem Bilan
*
* @since 6.4
*/
public class ControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler> {
private Long sendTimeout;
public void setSendTimeout(Long sendTimeout) {
this.sendTimeout = sendTimeout;
}
@Override
protected MessageHandler createHandler() {
ServiceActivatingHandler handler = new ServiceActivatingHandler(new ControlBusMessageProcessor());
if (this.sendTimeout != null) {
handler.setSendTimeout(this.sendTimeout);
}
return handler;
}
}

View File

@@ -53,6 +53,7 @@ import org.springframework.integration.support.channel.BeanFactoryChannelResolve
import org.springframework.integration.support.channel.ChannelResolverUtils;
import org.springframework.integration.support.converter.ConfigurableCompositeMessageConverter;
import org.springframework.integration.support.converter.DefaultDatatypeChannelMessageConverter;
import org.springframework.integration.support.management.ControlBusCommandRegistry;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ClassUtils;
@@ -127,6 +128,7 @@ public class DefaultConfiguringBeanFactoryPostProcessor implements BeanDefinitio
registerMessageHandlerMethodFactory();
registerListMessageHandlerMethodFactory();
registerIntegrationConfigurationReport();
registerControlBusCommandRegistry();
}
@Override
@@ -440,6 +442,17 @@ public class DefaultConfiguringBeanFactoryPostProcessor implements BeanDefinitio
.getBeanDefinition());
}
private void registerControlBusCommandRegistry() {
if (!this.beanFactory.containsBean(IntegrationContextUtils.CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME)) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(ControlBusCommandRegistry.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
this.registry.registerBeanDefinition(IntegrationContextUtils.CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME,
builder.getBeanDefinition());
}
}
private static BeanDefinitionBuilder createMessageHandlerMethodFactoryBeanDefinition(boolean listCapable) {
return BeanDefinitionBuilder.genericBeanDefinition(IntegrationMessageHandlerMethodFactory.class)
.addConstructorArgValue(listCapable)

View File

@@ -73,4 +73,12 @@ public @interface EnableIntegrationManagement {
*/
String[] observationPatterns() default {};
/**
* Set to {@code true} to turn on Control Bus commands loading after application context initialization.
* @return the flag to initialize the control bus registry eagerly or not.
* @since 6.4
* @see org.springframework.integration.support.management.ControlBusCommandRegistry#setEagerInitialization(boolean)
*/
String loadControlBusCommands() default "false";
}

View File

@@ -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.
@@ -17,8 +17,6 @@
package org.springframework.integration.config;
import org.springframework.expression.MethodFilter;
import org.springframework.integration.expression.ControlBusMethodFilter;
import org.springframework.integration.handler.ExpressionCommandMessageProcessor;
import org.springframework.integration.handler.ServiceActivatingHandler;
import org.springframework.messaging.MessageHandler;
@@ -31,10 +29,14 @@ import org.springframework.messaging.MessageHandler;
* @author Artem Bilan
*
* @since 2.0
*
* @deprecated in favor of {@link ControlBusFactoryBean}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class ExpressionControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler> {
private static final MethodFilter METHOD_FILTER = new ControlBusMethodFilter();
@SuppressWarnings("removal")
private static final MethodFilter METHOD_FILTER = new org.springframework.integration.expression.ControlBusMethodFilter();
private Long sendTimeout;
@@ -42,10 +44,11 @@ public class ExpressionControlBusFactoryBean extends AbstractSimpleMessageHandle
this.sendTimeout = sendTimeout;
}
@SuppressWarnings("removal")
@Override
protected MessageHandler createHandler() {
ExpressionCommandMessageProcessor processor =
new ExpressionCommandMessageProcessor(METHOD_FILTER, getBeanFactory());
org.springframework.integration.handler.ExpressionCommandMessageProcessor processor =
new org.springframework.integration.handler.ExpressionCommandMessageProcessor(METHOD_FILTER, getBeanFactory());
ServiceActivatingHandler handler = new ServiceActivatingHandler(processor);
if (this.sendTimeout != null) {
handler.setSendTimeout(this.sendTimeout);

View File

@@ -33,6 +33,7 @@ import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.integration.support.management.ControlBusCommandRegistry;
import org.springframework.integration.support.management.metrics.MetricsCaptor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -52,10 +53,16 @@ import org.springframework.util.StringUtils;
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class IntegrationManagementConfiguration implements ImportAware, EnvironmentAware {
private final ControlBusCommandRegistry controlBusCommandRegistry;
private AnnotationAttributes attributes;
private Environment environment;
public IntegrationManagementConfiguration(ControlBusCommandRegistry controlBusCommandRegistry) {
this.controlBusCommandRegistry = controlBusCommandRegistry;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
@@ -67,6 +74,9 @@ public class IntegrationManagementConfiguration implements ImportAware, Environm
this.attributes = AnnotationAttributes.fromMap(map);
Assert.notNull(this.attributes, () ->
"@EnableIntegrationManagement is not present on importing class " + importMetadata.getClassName());
this.controlBusCommandRegistry.setEagerInitialization(
Boolean.parseBoolean(
this.environment.resolvePlaceholders(this.attributes.getString("loadControlBusCommands"))));
}
@Bean(name = IntegrationManagementConfigurer.MANAGEMENT_CONFIGURER_NAME)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -20,19 +20,30 @@ import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ControlBusFactoryBean;
/**
* @author Dave Syer
* @author Oleg Zhurakousky
* @author Artem Bilan
*
* @since 2.0
*/
public class ControlBusParser extends AbstractConsumerEndpointParser {
@Override
@SuppressWarnings("removal")
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.config.ExpressionControlBusFactoryBean");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "bean-resolver");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ControlBusFactoryBean.class);
if (Boolean.FALSE.equals(Boolean.parseBoolean(element.getAttribute("use-registry")))) {
builder = BeanDefinitionBuilder.genericBeanDefinition(
org.springframework.integration.config.ExpressionControlBusFactoryBean.class);
parserContext.getReaderContext()
.warning("The 'ExpressionControlBusFactoryBean' for '<control-bus>' is deprecated (for removal) " +
"in favor of 'ControlBusFactoryBean'. " +
"Set 'use-registry' attribute to 'true' to switch to a new functionality.",
element);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");
return builder;

View File

@@ -100,6 +100,8 @@ public abstract class IntegrationContextUtils {
public static final String LIST_MESSAGE_HANDLER_FACTORY_BEAN_NAME = "integrationListMessageHandlerMethodFactory";
public static final String CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME = "controlBusCommandRegistry";
/**
* The default timeout for blocking operations like send and receive messages.
* @since 6.1

View File

@@ -52,7 +52,6 @@ import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.core.MessageSelector;
import org.springframework.integration.dsl.support.FixedSubscriberChannelPrototype;
import org.springframework.integration.dsl.support.MessageChannelReference;
import org.springframework.integration.expression.ControlBusMethodFilter;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.filter.ExpressionEvaluatingSelector;
import org.springframework.integration.filter.MessageFilter;
@@ -60,8 +59,8 @@ import org.springframework.integration.filter.MethodInvokingSelector;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.handler.BeanNameMessageProcessor;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.handler.ControlBusMessageProcessor;
import org.springframework.integration.handler.DelayHandler;
import org.springframework.integration.handler.ExpressionCommandMessageProcessor;
import org.springframework.integration.handler.LambdaMessageProcessor;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.handler.MessageProcessor;
@@ -517,8 +516,33 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
* at the current {@link IntegrationFlow} chain position.
* @return the current {@link BaseIntegrationFlowDefinition}.
* @see ExpressionCommandMessageProcessor
* @since 6.4
* @see ControlBusMessageProcessor
*/
public B controlBusOnRegistry() {
return controlBusOnRegistry(null);
}
/**
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
* at the current {@link IntegrationFlow} chain position.
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
* @return the current {@link BaseIntegrationFlowDefinition}.
* @since 6.4
* @see GenericEndpointSpec
* @see ControlBusMessageProcessor
*/
public B controlBusOnRegistry(@Nullable Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
return handle(new ServiceActivatingHandler(new ControlBusMessageProcessor()), endpointConfigurer);
}
/**
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
* at the current {@link IntegrationFlow} chain position.
* @return the current {@link BaseIntegrationFlowDefinition}.
* @deprecated in favor of {@link #controlBusOnRegistry()}
*/
@Deprecated(since = "6.4", forRemoval = true)
public B controlBus() {
return controlBus(null);
}
@@ -528,12 +552,15 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
* at the current {@link IntegrationFlow} chain position.
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
* @return the current {@link BaseIntegrationFlowDefinition}.
* @see ExpressionCommandMessageProcessor
* @deprecated in favor of {@link #controlBusOnRegistry(Consumer)}
* @see GenericEndpointSpec
*/
@Deprecated(since = "6.4", forRemoval = true)
@SuppressWarnings("removal")
public B controlBus(@Nullable Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
return handle(new ServiceActivatingHandler(new ExpressionCommandMessageProcessor(
new ControlBusMethodFilter())), endpointConfigurer);
return handle(new ServiceActivatingHandler(
new org.springframework.integration.handler.ExpressionCommandMessageProcessor(
new org.springframework.integration.expression.ControlBusMethodFilter())), endpointConfigurer);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -21,9 +21,6 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.context.Lifecycle;
import org.springframework.core.annotation.AnnotationFilter;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.expression.MethodFilter;
import org.springframework.integration.core.Pausable;
import org.springframework.jmx.export.annotation.ManagedAttribute;
@@ -45,9 +42,15 @@ import org.springframework.util.ReflectionUtils;
* @author Gary Russell
*
* @since 4.0
*
* @deprecated in favor of {@link org.springframework.integration.support.management.ControlBusMethodFilter}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class ControlBusMethodFilter implements MethodFilter {
private static final ReflectionUtils.MethodFilter CONTROL_BUS_METHOD_FILTER =
new org.springframework.integration.support.management.ControlBusMethodFilter();
@Override
public List<Method> filter(List<Method> methods) {
List<Method> supportedMethods = new ArrayList<>();
@@ -60,26 +63,7 @@ public class ControlBusMethodFilter implements MethodFilter {
}
private boolean accept(Method method) {
Class<?> declaringClass = method.getDeclaringClass();
String methodName = method.getName();
if ((Pausable.class.isAssignableFrom(declaringClass) || Lifecycle.class.isAssignableFrom(declaringClass))
&& ReflectionUtils.findMethod(Pausable.class, methodName, method.getParameterTypes()) != null) {
return true;
}
if (CustomizableThreadCreator.class.isAssignableFrom(declaringClass)
&& (methodName.startsWith("get")
|| methodName.startsWith("set")
|| methodName.startsWith("shutdown"))) {
return true;
}
MergedAnnotations mergedAnnotations =
MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none(), AnnotationFilter.PLAIN);
return mergedAnnotations.get(ManagedAttribute.class).isPresent()
|| mergedAnnotations.get(ManagedOperation.class).isPresent();
return CONTROL_BUS_METHOD_FILTER.matches(method);
}
}

View File

@@ -58,6 +58,10 @@ public abstract class AbstractMessageProcessingSelector
}
}
protected MessageProcessor<Boolean> getMessageProcessor() {
return this.messageProcessor;
}
@Override
public final boolean accept(Message<?> message) {
Object result = this.messageProcessor.processMessage(message);

View File

@@ -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.
@@ -33,6 +33,8 @@ import org.springframework.integration.handler.ExpressionEvaluatingMessageProces
* @author Artem Bilan
*
* @since 2.0
*
* @see SimpleExpressionEvaluatingSelector
*/
public class ExpressionEvaluatingSelector extends AbstractMessageProcessingSelector {

View File

@@ -0,0 +1,58 @@
/*
* 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.filter;
import org.springframework.expression.Expression;
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
/**
* A {@link org.springframework.integration.core.MessageSelector} implementation that
* evaluates a simple SpEL expression - relies on the
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext}.
*
* @author Artem Bilan
*
* @since 6.4
*
* @see ExpressionEvaluatingSelector
*/
public class SimpleExpressionEvaluatingSelector extends AbstractMessageProcessingSelector {
private final String expressionString;
public SimpleExpressionEvaluatingSelector(String expressionString) {
super(new ExpressionEvaluatingMessageProcessor<>(expressionString, Boolean.class));
((ExpressionEvaluatingMessageProcessor<?>) getMessageProcessor()).setSimpleEvaluationContext(true);
this.expressionString = expressionString;
}
public SimpleExpressionEvaluatingSelector(Expression expression) {
super(new ExpressionEvaluatingMessageProcessor<>(expression, Boolean.class));
((ExpressionEvaluatingMessageProcessor<?>) getMessageProcessor()).setSimpleEvaluationContext(true);
this.expressionString = expression.getExpressionString();
}
public String getExpressionString() {
return this.expressionString;
}
@Override
public String toString() {
return "SimpleExpressionEvaluatingSelector for: [" + this.expressionString + "]";
}
}

View File

@@ -35,8 +35,10 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.convert.ConversionService;
@@ -614,6 +616,14 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
if (processor instanceof BeanFactoryAware beanFactoryAware && beanFactory != null) {
beanFactoryAware.setBeanFactory(beanFactory);
}
if (processor instanceof InitializingBean initializingBean) {
try {
initializingBean.afterPropertiesSet();
}
catch (Exception ex) {
throw new BeanCreationException("Cannot initialize processor for: " + this, ex);
}
}
if (!this.async && processor instanceof MethodInvokingMessageProcessor<?> methodInvokingMessageProcessor) {
this.async = methodInvokingMessageProcessor.isAsync();
}

View File

@@ -0,0 +1,88 @@
/*
* 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.handler;
import java.util.List;
import org.springframework.expression.Expression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.IntegrationPattern;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.integration.support.management.ControlBusCommandRegistry;
import org.springframework.messaging.Message;
import org.springframework.util.CollectionUtils;
/**
* A MessageProcessor implementation that expects a Control Bus command as a request message.
* When processing, it evaluates a SpEL expression associated with requested command,
* essentially target bean method invocation.
* <p>
* The arguments for the command must be provided
* in the {@link IntegrationMessageHeaderAccessor#CONTROL_BUS_ARGUMENTS} message header.
*
* @author Artem Bilan
*
* @since 6.4
*/
public class ControlBusMessageProcessor extends AbstractMessageProcessor<Object>
implements IntegrationPattern {
private ControlBusCommandRegistry controlBusCommandRegistry;
public ControlBusMessageProcessor() {
}
/**
* Create an instance based on the provided {@link ControlBusCommandRegistry}.
* @param controlBusCommandRegistry the {@link ControlBusCommandRegistry} with commands to execute.
*/
public ControlBusMessageProcessor(ControlBusCommandRegistry controlBusCommandRegistry) {
this.controlBusCommandRegistry = controlBusCommandRegistry;
}
@Override
public IntegrationPatternType getIntegrationPatternType() {
return IntegrationPatternType.control_bus;
}
@Override
protected void onInit() {
super.onInit();
if (this.controlBusCommandRegistry == null) {
this.controlBusCommandRegistry = getBeanFactory().getBean(ControlBusCommandRegistry.class);
}
}
@Override
public Object processMessage(Message<?> message) {
String command = message.getPayload().toString();
@SuppressWarnings("unchecked")
List<Object> arguments =
message.getHeaders().get(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.class);
Class<?>[] parameterTypes = new Class<?>[0];
if (!CollectionUtils.isEmpty(arguments)) {
parameterTypes =
arguments.stream()
.map(Object::getClass)
.toArray(Class<?>[]::new);
}
Expression commandExpression = this.controlBusCommandRegistry.getExpressionForCommand(command, parameterTypes);
return evaluateExpression(commandExpression, arguments);
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodFilter;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.spel.support.ReflectiveMethodResolver;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.IntegrationPattern;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.lang.Nullable;
@@ -47,7 +48,10 @@ import org.springframework.util.CollectionUtils;
* @author Artem Bilan
*
* @since 2.0
*
* @deprecated in favor of {@link ControlBusMessageProcessor}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class ExpressionCommandMessageProcessor extends AbstractMessageProcessor<Object>
implements IntegrationPattern {
@@ -74,7 +78,12 @@ public class ExpressionCommandMessageProcessor extends AbstractMessageProcessor<
super.setBeanFactory(beanFactory);
if (this.methodFilter != null) {
MethodResolver methodResolver = new ExpressionCommandMethodResolver(this.methodFilter);
getEvaluationContext().setMethodResolvers(Collections.singletonList(methodResolver));
if (getEvaluationContext() instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setMethodResolvers(Collections.singletonList(methodResolver));
}
else {
logger.warn("Cannot customize the 'SimpleEvaluationContext'");
}
}
}

View File

@@ -62,6 +62,7 @@ import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
@@ -419,27 +420,30 @@ public class MessagingMethodInvokerHelper extends AbstractExpressionEvaluator im
}
private void prepareEvaluationContext() {
StandardEvaluationContext context = getEvaluationContext();
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
if (this.method != null) {
context.registerMethodFilter(targetType,
new FixedMethodFilter(ClassUtils.getMostSpecificMethod(this.method, targetType)));
if (this.expectedType != null) {
Assert.state(context.getTypeConverter()
.canConvert(TypeDescriptor.valueOf((this.method).getReturnType()), this.expectedType),
() -> "Cannot convert to expected type (" + this.expectedType + ") from " + this.method);
EvaluationContext context = getEvaluationContext();
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
if (this.method != null) {
standardEvaluationContext.registerMethodFilter(targetType,
new FixedMethodFilter(ClassUtils.getMostSpecificMethod(this.method, targetType)));
if (this.expectedType != null) {
Assert.state(context.getTypeConverter()
.canConvert(TypeDescriptor.valueOf((this.method).getReturnType()), this.expectedType),
() -> "Cannot convert to expected type (" + this.expectedType + ") from " + this.method);
}
}
else {
AnnotatedMethodFilter filter = new AnnotatedMethodFilter(this.annotationType, this.methodName,
this.requiresReply);
Assert.state(canReturnExpectedType(filter, targetType, context.getTypeConverter()),
() -> "Cannot convert to expected type (" + this.expectedType + ") from " + this.methodName);
standardEvaluationContext.registerMethodFilter(targetType, filter);
}
}
else {
AnnotatedMethodFilter filter = new AnnotatedMethodFilter(this.annotationType, this.methodName,
this.requiresReply);
Assert.state(canReturnExpectedType(filter, targetType, context.getTypeConverter()),
() -> "Cannot convert to expected type (" + this.expectedType + ") from " + this.methodName);
context.registerMethodFilter(targetType, filter);
}
context.setVariable("target", this.targetObject);
try {
context.registerFunction("requiredHeader",
context.setVariable("requiredHeader",
ParametersWrapper.class.getDeclaredMethod("getHeader", Map.class, String.class));
}
catch (NoSuchMethodException ex) {

View File

@@ -34,6 +34,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.integration.IntegrationPatternType;
import org.springframework.integration.core.MessageSelector;
import org.springframework.integration.filter.ExpressionEvaluatingSelector;
import org.springframework.integration.filter.SimpleExpressionEvaluatingSelector;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.lang.Nullable;
@@ -135,8 +136,8 @@ public class RecipientListRouter extends AbstractMessageRouter implements Recipi
private void addRecipient(String channelName, String selectorExpression, Queue<Recipient> recipientsToAdd) {
Assert.hasText(channelName, "'channelName' must not be empty.");
Assert.hasText(selectorExpression, "'selectorExpression' must not be empty.");
ExpressionEvaluatingSelector expressionEvaluatingSelector =
new ExpressionEvaluatingSelector(selectorExpression);
SimpleExpressionEvaluatingSelector expressionEvaluatingSelector =
new SimpleExpressionEvaluatingSelector(selectorExpression);
expressionEvaluatingSelector.setBeanFactory(getBeanFactory());
Recipient recipient = new Recipient(channelName, expressionEvaluatingSelector);
setupRecipient(recipient);
@@ -203,9 +204,13 @@ public class RecipientListRouter extends AbstractMessageRouter implements Recipi
Recipient next = it.next();
MessageSelector selector = next.getSelector();
MessageChannel channel = next.getChannel();
if (selector instanceof ExpressionEvaluatingSelector
if ((selector instanceof ExpressionEvaluatingSelector expressionEvaluatingSelector
&& targetChannel.equals(channel)
&& ((ExpressionEvaluatingSelector) selector).getExpressionString().equals(selectorExpression)) {
&& expressionEvaluatingSelector.getExpressionString().equals(selectorExpression)) ||
(selector instanceof SimpleExpressionEvaluatingSelector simpleExpressionEvaluatingSelector
&& targetChannel.equals(channel)
&& simpleExpressionEvaluatingSelector.getExpressionString().equals(selectorExpression))) {
it.remove();
counter++;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 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.
@@ -17,9 +17,11 @@
package org.springframework.integration.router;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.springframework.integration.core.MessageSelector;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
@@ -31,6 +33,8 @@ import org.springframework.jmx.export.annotation.ManagedResource;
*
* @author Liujiong
* @author Gary Russell
* @author Artem Bilan
*
* @since 4.1
*
*/
@@ -40,6 +44,10 @@ public interface RecipientListRouterManagement {
/**
* Add a recipient with channelName and expression.
* The expression follows only
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext#forReadOnlyDataBinding()}
* capabilities. Otherwise, use non-managed {@link RecipientListRouter#addRecipient(String, MessageSelector)}
* API with more control over execution.
* @param channelName The channel name.
* @param selectorExpression The expression to filter the incoming message.
*/
@@ -78,6 +86,10 @@ public interface RecipientListRouterManagement {
/**
* Replace recipient.
* The expression follows only
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext#forReadOnlyDataBinding()}
* capabilities. Otherwise, use non-managed {@link RecipientListRouter#addRecipient(String, MessageSelector)}
* API with more control over execution.
* @param recipientMappings contain channelName and expression.
*/
@ManagedOperation
@@ -85,6 +97,10 @@ public interface RecipientListRouterManagement {
/**
* Set recipients.
* The expression follows only
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext#forReadOnlyDataBinding()}
* capabilities. Otherwise, use non-managed {@link RecipientListRouter#setRecipients(List)}
* API with more control over execution.
* @param recipientMappings contain channelName and expression.
*/
@ManagedAttribute

View File

@@ -0,0 +1,334 @@
/*
* 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.management;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.Lifecycle;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CustomizableThreadCreator;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* A global component to serve Control Bus command and respective SpEL expression relationships.
*
* @author Artem Bilan
*
* @since 6.4
*
* @see ControlBusMethodFilter
*/
public class ControlBusCommandRegistry
implements ApplicationContextAware, SmartInitializingSingleton, DestructionAwareBeanPostProcessor {
private static final Pattern COMMAND_PATTERN =
Pattern.compile("^@?'?(?<beanName>.[^']+)'?\\.(?<methodName>[a-zA-Z0-9_]+)\\(?\\)?$");
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final ControlBusMethodFilter CONTROL_BUS_METHOD_FILTER = new ControlBusMethodFilter();
private final Map<String, Map<CommandMethod, Expression>> controlBusCommands = new HashMap<>();
private boolean eagerInitialization;
private ApplicationContext applicationContext;
private boolean initialized;
/**
* Set to {@code true} to turn on Control Bus commands loading after application context initialization.
* @param eagerInitialization true to initialize this registry eagerly.
*/
public void setEagerInitialization(boolean eagerInitialization) {
this.eagerInitialization = eagerInitialization;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterSingletonsInstantiated() {
if (this.eagerInitialization) {
this.applicationContext.getBeansOfType(null)
.forEach(this::registerControlBusCommands);
this.initialized = true;
}
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Only for dynamically created beans. Others are registered by afterSingletonsInstantiated()
if (this.initialized) {
registerControlBusCommands(beanName, bean);
}
return bean;
}
/**
* Manually register Control Bus commands (if any) for a specific bean.
* The bean must be in instance of {@link Lifecycle}, or {@link CustomizableThreadCreator},
* or marked with the {@link ManagedResource}, or {@link IntegrationManagedResource} annotation.
* @param beanName the bean name for registration
* @param bean the bean for registration
*/
public void registerControlBusCommands(String beanName, Object bean) {
Class<?> beanClass = bean.getClass();
if (bean instanceof Lifecycle || bean instanceof CustomizableThreadCreator
|| AnnotationUtils.findAnnotation(beanClass, ManagedResource.class) != null
|| AnnotationUtils.findAnnotation(beanClass, IntegrationManagedResource.class) != null) {
ReflectionUtils.doWithMethods(beanClass, method -> populateExpressionForCommand(beanName, method),
CONTROL_BUS_METHOD_FILTER);
}
}
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
this.controlBusCommands.remove(beanName);
}
/**
* Return registered Control Bus commands.
* @return registered commands.
*/
public Map<String, Map<CommandMethod, String>> getCommands() {
Map<String, Map<CommandMethod, String>> commands = new HashMap<>(this.controlBusCommands.size());
for (Map.Entry<String, Map<CommandMethod, Expression>> beanEntry : this.controlBusCommands.entrySet()) {
Map<CommandMethod, String> commandEntries =
beanEntry.getValue()
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
(commandEntry) -> commandEntry.getValue().getExpressionString()));
commands.put(beanEntry.getKey(), commandEntries);
}
return Collections.unmodifiableMap(commands);
}
/**
* Obtain a SpEL expression for the command to call with parameter types.
* The command must be in format {@code beanName.methodName}.
* (Or {@code @beanName.methodName()} for backward compatibility with simple expressions.)
* If {@code beanName} is a complex literal, it has to be wrapped into single quotes,
* e.g. {@code 'some.complex.bean-name'}.
* The target method to call must fit into the {@link ControlBusMethodFilter} requirements
* and match with the provided parameter types.
* @param command the command to call.
* @param parameterTypes the parameter types for the target method to call.
* @return the SpEL expression for the provided command.
* @throws IllegalArgumentException if provided command does not match to Control Bus requirements
* or target method contract.
*/
public Expression getExpressionForCommand(String command, Class<?>... parameterTypes) {
Matcher matcher = COMMAND_PATTERN.matcher(command);
Assert.isTrue(matcher.matches(),
"The command must be in format 'beanName.methodName'. " +
"Arguments must go to the 'controlBusArguments' message header.");
String beanName = matcher.group("beanName");
Assert.isTrue(this.applicationContext.containsBean(beanName),
() -> "There is no registered bean for requested command: " + beanName);
String methodName = matcher.group("methodName");
CommandMethod commandMethod = new CommandMethod(beanName, methodName, parameterTypes);
return populateCommandMethod(commandMethod, (key) -> buildExpressionForMethodToCall(commandMethod));
}
private void populateExpressionForCommand(String beanName, Method methodForCommand) {
CommandMethod commandMethod =
new CommandMethod(beanName, methodForCommand.getName(), methodForCommand.getParameterTypes());
populateCommandMethod(commandMethod, (key) -> buildExpressionForMethodToCall(commandMethod, methodForCommand));
}
private Expression populateCommandMethod(CommandMethod commandMethod,
Function<CommandMethod, Expression> mappingFunction) {
String beanName = commandMethod.beanName;
Map<CommandMethod, Expression> beanControlBusCommands =
this.controlBusCommands.computeIfAbsent(beanName, (key) -> new HashMap<>());
try {
return beanControlBusCommands.computeIfAbsent(commandMethod, mappingFunction);
}
catch (IllegalArgumentException ex) {
if (beanControlBusCommands.isEmpty()) {
this.controlBusCommands.remove(beanName);
}
throw ex;
}
}
private Expression buildExpressionForMethodToCall(CommandMethod commandMethod) {
Object bean = this.applicationContext.getBean(commandMethod.beanName);
Set<Method> candidates = MethodIntrospector.selectMethods(bean.getClass(),
(ReflectionUtils.MethodFilter) method -> commandMethod.methodName.equals(method.getName()));
Optional<Method> methodForCommand =
candidates.stream()
.filter(method ->
areParameterTypesEqual(method.getParameterTypes(), commandMethod.parameterTypes))
.findFirst();
Assert.isTrue(methodForCommand.isPresent(),
() -> "No method '%s' found in bean '%s' for parameter types '%s'"
.formatted(commandMethod.methodName, bean, Arrays.toString(commandMethod.parameterTypes)));
return buildExpressionForMethodToCall(commandMethod, methodForCommand.get());
}
private static Expression buildExpressionForMethodToCall(CommandMethod commandMethod, Method methodForCommand) {
Assert.isTrue(CONTROL_BUS_METHOD_FILTER.matches(methodForCommand),
() -> "The method '%s' is not valid Control Bus command".formatted(methodForCommand));
populateDescriptionIntoCommand(commandMethod, methodForCommand);
Class<?>[] parameterTypes = methodForCommand.getParameterTypes();
StringBuilder expressionBuilder =
new StringBuilder("@'")
.append(commandMethod.beanName)
.append("'.")
.append(methodForCommand.getName())
.append('(');
for (int i = 0; i < parameterTypes.length; i++) {
expressionBuilder.append('[').append(i).append("],");
}
if (parameterTypes.length > 0) {
expressionBuilder.deleteCharAt(expressionBuilder.length() - 1);
}
String expression = expressionBuilder.append(')').toString();
return EXPRESSION_PARSER.parseExpression(expression);
}
private static boolean areParameterTypesEqual(Class<?>[] lhsTypes, Class<?>[] rhsTypes) {
if (lhsTypes.length == rhsTypes.length) {
for (int i = 0; i < lhsTypes.length; i++) {
if (!ClassUtils.isAssignable(lhsTypes[i], rhsTypes[i])) {
return false;
}
}
return true;
}
return false;
}
private static void populateDescriptionIntoCommand(CommandMethod commandMethod, Method methodForCommand) {
ManagedOperation managedOperation = AnnotationUtils.findAnnotation(methodForCommand, ManagedOperation.class);
if (managedOperation != null) {
commandMethod.description = managedOperation.description();
}
else {
ManagedAttribute managedAttribute = AnnotationUtils.findAnnotation(methodForCommand, ManagedAttribute.class);
if (managedAttribute != null) {
commandMethod.description = managedAttribute.description();
}
}
if (!StringUtils.hasText(commandMethod.description)) {
commandMethod.description = commandMethod.methodName;
}
}
/**
* The Java Bean to represent a Control Bus command as a bean method with its parameter types.
*/
public static final class CommandMethod {
private final String beanName;
private final String methodName;
private final Class<?>[] parameterTypes;
private String description;
private CommandMethod(String beanName, String methodName, Class<?>[] parameterTypes) {
this.beanName = beanName;
this.methodName = methodName;
this.parameterTypes = parameterTypes;
}
public String getBeanName() {
return this.beanName;
}
public String getMethodName() {
return this.methodName;
}
public Class<?>[] getParameterTypes() {
return this.parameterTypes;
}
public String getDescription() {
return this.description;
}
@Override
public int hashCode() {
int result = Objects.hash(this.beanName, this.methodName);
result = 31 * result + Arrays.hashCode(this.parameterTypes);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CommandMethod that = (CommandMethod) o;
return Objects.equals(this.beanName, that.beanName)
&& Objects.equals(this.methodName, that.methodName)
&& areParameterTypesEqual(this.parameterTypes, that.parameterTypes);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.
* 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.management;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.context.Lifecycle;
import org.springframework.core.annotation.AnnotationFilter;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.integration.core.Pausable;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.util.CustomizableThreadCreator;
import org.springframework.util.ReflectionUtils;
/**
* The {@link ReflectionUtils.MethodFilter} to restrict method invocations to:
* <ul>
* <li> {@link Pausable} or {@link Lifecycle} components
* <li> {@code get}, {@code set} and {@code shutdown} methods of {@link CustomizableThreadCreator}
* <li> methods with {@link ManagedAttribute} or {@link ManagedOperation} annotations
* </ul>
*
* @author Artem Bilan
*
* @since 6.4
*/
public class ControlBusMethodFilter implements ReflectionUtils.MethodFilter {
@Override
public boolean matches(Method method) {
if (Modifier.isPublic(method.getModifiers())) {
Class<?> declaringClass = method.getDeclaringClass();
String methodName = method.getName();
if ((Pausable.class.isAssignableFrom(declaringClass) || Lifecycle.class.isAssignableFrom(declaringClass))
&& ReflectionUtils.findMethod(Pausable.class, methodName, method.getParameterTypes()) != null) {
return true;
}
if (CustomizableThreadCreator.class.isAssignableFrom(declaringClass)
&& (methodName.startsWith("get")
|| methodName.startsWith("set")
|| methodName.equals("shutdown"))) {
return true;
}
MergedAnnotations mergedAnnotations =
MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
RepeatableContainers.none(), AnnotationFilter.PLAIN);
return mergedAnnotations.get(ManagedAttribute.class).isPresent()
|| mergedAnnotations.get(ManagedOperation.class).isPresent();
}
return false;
}
}

View File

@@ -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.
@@ -19,9 +19,9 @@ package org.springframework.integration.util;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.log.LogAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
@@ -51,7 +51,9 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
private final BeanFactoryTypeConverter typeConverter = new BeanFactoryTypeConverter();
private volatile StandardEvaluationContext evaluationContext;
private boolean simpleEvaluationContext;
private volatile EvaluationContext evaluationContext;
private volatile BeanFactory beanFactory;
@@ -64,9 +66,6 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
this.typeConverter.setBeanFactory(beanFactory);
if (this.evaluationContext != null && this.evaluationContext.getBeanResolver() == null) {
this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
}
protected BeanFactory getBeanFactory() {
@@ -83,6 +82,17 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
return this.messageBuilderFactory;
}
/**
* The flag to indicate that a {@link org.springframework.expression.spel.support.SimpleEvaluationContext}
* must be used for expression evaluations.
* @param simpleEvaluationContext true to use the
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext}
* @since 6.4
*/
public void setSimpleEvaluationContext(boolean simpleEvaluationContext) {
this.simpleEvaluationContext = simpleEvaluationContext;
}
@Override
public final void afterPropertiesSet() {
getEvaluationContext();
@@ -93,24 +103,21 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
onInit();
}
protected StandardEvaluationContext getEvaluationContext() {
protected EvaluationContext getEvaluationContext() {
return getEvaluationContext(true);
}
/**
* Emits a WARN log if the beanFactory field is null, unless the argument is false.
* @param beanFactoryRequired set to false to suppress the warning.
* @param beanFactoryRequired set to {@code false} to suppress the warning.
* @return The evaluation context.
*/
protected final StandardEvaluationContext getEvaluationContext(boolean beanFactoryRequired) {
protected final EvaluationContext getEvaluationContext(boolean beanFactoryRequired) {
if (this.evaluationContext == null) {
if (this.beanFactory == null && !beanFactoryRequired) {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext();
this.evaluationContext = obtainEvaluationContext(beanFactoryRequired);
if (this.evaluationContext instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setTypeConverter(this.typeConverter);
}
else {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
this.evaluationContext.setTypeConverter(this.typeConverter);
if (this.beanFactory != null) {
ConversionService conversionService = IntegrationUtils.getConversionService(this.beanFactory);
if (conversionService != null) {
@@ -121,6 +128,20 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
return this.evaluationContext;
}
private EvaluationContext obtainEvaluationContext(boolean beanFactoryRequired) {
if (this.beanFactory == null && !beanFactoryRequired) {
return
this.simpleEvaluationContext
? ExpressionUtils.createSimpleEvaluationContext()
: ExpressionUtils.createStandardEvaluationContext();
}
else {
return this.simpleEvaluationContext
? ExpressionUtils.createSimpleEvaluationContext(this.beanFactory)
: ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
}
}
@Nullable
protected <T> T evaluateExpression(Expression expression, Message<?> message, @Nullable Class<T> expectedType) {
try {

View File

@@ -283,7 +283,19 @@ class KotlinIntegrationFlowDefinition(@PublishedApi internal val delegate: Integ
/**
* Populate the `Control Bus` EI Pattern specific [MessageHandler] implementation
* at the current [IntegrationFlow] chain position.
* @since 6.4
*/
fun controlBusOnRegistry(endpointConfigurer: GenericEndpointSpec<ServiceActivatingHandler>.() -> Unit = {}) {
this.delegate.controlBusOnRegistry(endpointConfigurer)
}
/**
* Populate the `Control Bus` EI Pattern specific [MessageHandler] implementation
* at the current [IntegrationFlow] chain position.
*/
@Deprecated("Use 'controlBusOnRegistry()' instead.",
replaceWith = ReplaceWith("controlBusOnRegistry()"))
@Suppress("DEPRECATION", "REMOVAL")
fun controlBus(endpointConfigurer: GenericEndpointSpec<ServiceActivatingHandler>.() -> Unit = {}) {
this.delegate.controlBus(endpointConfigurer)
}

View File

@@ -4968,6 +4968,16 @@ The list of component name patterns you want to track (e.g., tracked-components
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="id" type="xsd:string"/>
<xsd:attribute name="use-registry" type="xsd:boolean" default="false">
<xsd:annotation>
<xsd:documentation>
Set true to make Control Bus based on the global 'ControlBusCommandRegistry'
which is a recommended way to configure Control Bus functionality.
The false is by default for backward compatibility and is deprecated.
This attributed will be true by default in the next major version and removed altogether eventually.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="retryAdviceType">

View File

@@ -20,6 +20,6 @@
<beans:bean id="aggregatorBean"
class="org.springframework.integration.config.TestAggregatorBean" />
<control-bus input-channel="controlBusChannel" output-channel="nullChannel"/>
<control-bus input-channel="controlBusChannel" output-channel="nullChannel" use-registry="true"/>
</beans:beans>

View File

@@ -16,19 +16,19 @@
package org.springframework.integration.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,9 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Dave Syer
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
@SpringJUnitConfig
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class AggregatorWithMessageStoreParserTests {
@Autowired
@@ -55,7 +54,6 @@ public class AggregatorWithMessageStoreParserTests {
private MessageChannel controlBusChannel;
@Test
@DirtiesContext
public void testAggregation() {
input.send(createMessage("123", "id1", 3, 1, null));
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(1);
@@ -71,13 +69,15 @@ public class AggregatorWithMessageStoreParserTests {
}
@Test
@DirtiesContext
public void testExpiry() {
input.send(createMessage("123", "id1", 3, 1, null));
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(1);
input.send(createMessage("456", "id1", 3, 2, null));
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(2);
this.controlBusChannel.send(new GenericMessage<Object>("@messageStore.expireMessageGroups(-10000)"));
this.controlBusChannel.send(
MessageBuilder.withPayload("messageStore.expireMessageGroups")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(-10000L))
.build());
assertThat(aggregatorBean
.getAggregatedMessages().size()).as("One and only one message should have been aggregated")
.isEqualTo(1);
@@ -88,6 +88,7 @@ public class AggregatorWithMessageStoreParserTests {
private static <T> Message<T> createMessage(T payload, Object correlationId, int sequenceSize, int sequenceNumber,
MessageChannel outputChannel) {
return MessageBuilder.withPayload(payload)
.setCorrelationId(correlationId)
.setSequenceSize(sequenceSize)

View File

@@ -14,7 +14,7 @@
<queue />
</channel>
<control-bus input-channel="input" output-channel="output">
<control-bus input-channel="input" output-channel="output" use-registry="true">
<poller fixed-rate="100" />
</control-bus>

View File

@@ -16,17 +16,19 @@
package org.springframework.integration.config.xml;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @since 2.0
*/
@RunWith(SpringRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class ControlBusExplicitPollerTests {
@@ -49,19 +51,18 @@ public class ControlBusExplicitPollerTests {
@Test
public void testDefaultEvaluationContext() {
Message<?> message =
MessageBuilder.withPayload("@service.convert('aardvark')+headers.foo")
.setHeader("foo", "bar")
MessageBuilder.withPayload("service.convert")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("aardvark", "bar"))
.build();
this.input.send(message);
assertThat(output.receive(1000).getPayload()).isEqualTo("catbar");
assertThat(output.receive(0)).isNull();
}
public static class Service {
@ManagedOperation
public String convert(String input) {
return "cat";
public String convert(String input, String header) {
return "cat" + header;
}
}

View File

@@ -7,7 +7,7 @@
https://www.springframework.org/schema/beans/spring-beans.xsd">
<control-bus input-channel="inputChannel" auto-startup="true"/>
<control-bus input-channel="inputChannel" auto-startup="true" use-registry="true"/>
<inbound-channel-adapter id="adapter" channel="outputChannel" auto-startup="false" method="receive">
<poller fixed-rate="1000"/>

View File

@@ -14,7 +14,7 @@
<queue />
</channel>
<control-bus input-channel="input" output-channel="output"/>
<control-bus input-channel="input" output-channel="output" use-registry="true"/>
<poller default="true" fixed-rate="100"/>

View File

@@ -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,16 +16,18 @@
package org.springframework.integration.config.xml;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @since 2.0
*/
@RunWith(SpringRunner.class)
@SpringJUnitConfig
@DirtiesContext
public class ControlBusPollerTests {
@@ -48,18 +50,17 @@ public class ControlBusPollerTests {
@Test
public void testDefaultEvaluationContext() {
Message<?> message =
MessageBuilder.withPayload("@service.convert('aardvark')+headers.foo")
.setHeader("foo", "bar")
MessageBuilder.withPayload("service.convert")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("aardvark", "bar"))
.build();
this.input.send(message);
assertThat(output.receive(1000).getPayload()).isEqualTo("catbar");
assertThat(output.receive(0)).isNull();
}
public static class Service {
public String convert(String input) {
return "cat";
public String convert(String input, String header) {
return "cat" + header;
}
}

View File

@@ -10,7 +10,7 @@
<queue/>
</channel>
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="false"/>
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="false" use-registry="true"/>
<recipient-list-router id="simpleRouter" input-channel="routingChannelA"/>

View File

@@ -18,15 +18,17 @@ package org.springframework.integration.config.xml;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.router.RecipientListRouter.Recipient;
import org.springframework.integration.support.MessageBuilder;
@@ -36,17 +38,17 @@ import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Liujiong
* @author Artem Bilan
*
* @since 4.1
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class ControlBusRecipientListRouterTests {
@@ -63,63 +65,79 @@ public class ControlBusRecipientListRouterTests {
@Qualifier("routingChannelA")
private MessageChannel channel;
@Before
@BeforeEach
public void aa() {
context.start();
}
@Test
public void testAddRecipient() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel2','true')");
Message<?> message = new GenericMessage<Integer>(1);
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel2", "true"))
.build());
Message<?> message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel2 = (PollableChannel) context.getBean("channel2");
assertThat(chanel2.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel2.receive(0).getPayload()).isEqualTo(1);
}
@Test
public void testAddRecipientWithNullExpression() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel3')");
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel3"))
.build());
Message<?> message = new GenericMessage<Integer>(1);
Message<?> message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel3 = (PollableChannel) context.getBean("channel3");
assertThat(chanel3.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel3.receive(0).getPayload()).isEqualTo(1);
}
@Test
public void testRemoveRecipient() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1')");
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel4')");
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.removeRecipient('channel4')");
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1"))
.build());
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel4"))
.build());
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.removeRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel4"))
.build());
Message<?> message = new GenericMessage<Integer>(1);
Message<?> message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel1 = (PollableChannel) context.getBean("channel1");
PollableChannel chanel4 = (PollableChannel) context.getBean("channel4");
assertThat(chanel1.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel1.receive(0).getPayload()).isEqualTo(1);
assertThat(chanel4.receive(0)).isNull();
}
@Test
public void testRemoveRecipientWithExpression() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1','true')");
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel5','true')");
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.removeRecipient('channel5','true')");
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1", "true"))
.build());
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel5", "true"))
.build());
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.removeRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel5", "true"))
.build());
Message<?> message = new GenericMessage<Integer>(1);
Message<?> message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel1 = (PollableChannel) context.getBean("channel1");
PollableChannel chanel5 = (PollableChannel) context.getBean("channel5");
assertThat(chanel1.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel1.receive(0).getPayload()).isEqualTo(1);
assertThat(chanel5.receive(0)).isNull();
}
@@ -127,8 +145,10 @@ public class ControlBusRecipientListRouterTests {
@SuppressWarnings("unchecked")
public void testGetRecipients() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1')");
messagingTemplate.send(input,
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1"))
.build());
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.getRecipients()");
PollableChannel channel1 = (PollableChannel) context.getBean("channel1");
Message<?> result = this.output.receive(0);
@@ -138,29 +158,31 @@ public class ControlBusRecipientListRouterTests {
@Test
public void testSetRecipients() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
Map<String, String> map = new HashMap<String, String>();
Map<String, String> map = new HashMap<>();
map.put("channel6", "true");
Message<?> message = MessageBuilder.withPayload("@'simpleRouter.handler'.setRecipientMappings(headers.recipientMap)").setHeader("recipientMap", map).build();
Message<?> message =
MessageBuilder.withPayload("'simpleRouter.handler'.setRecipientMappings")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(map))
.build();
this.input.send(message);
message = new GenericMessage<Integer>(1);
message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel6 = (PollableChannel) context.getBean("channel6");
assertThat(chanel6.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel6.receive(0).getPayload()).isEqualTo(1);
}
@Test
public void testReplaceRecipients() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.replaceRecipients('channel7=true')");
Message<?> message = new GenericMessage<Integer>(1);
Properties newMapping = new Properties();
newMapping.setProperty("channel7", "true");
this.input.send(
MessageBuilder.withPayload("'simpleRouter.handler'.replaceRecipients")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
.build());
Message<?> message = new GenericMessage<>(1);
channel.send(message);
PollableChannel chanel7 = (PollableChannel) context.getBean("channel7");
assertThat(chanel7.receive(0).getPayload().equals(1)).isTrue();
assertThat(chanel7.receive(0).getPayload()).isEqualTo(1);
}
}

View File

@@ -15,7 +15,7 @@
<queue/>
</channel>
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true"/>
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true" use-registry="true"/>
<beans:bean id="service" class="org.springframework.integration.config.xml.ControlBusTests$Service" />

View File

@@ -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.
@@ -17,7 +17,9 @@
package org.springframework.integration.config.xml;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -26,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.DefaultHeaderChannelRegistry;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessagingTemplate;
@@ -68,17 +71,19 @@ public class ControlBusTests {
@Test
public void testDefaultEvaluationContext() {
Message<?> message =
MessageBuilder.withPayload("@service.convert('aardvark')+headers.foo")
.setHeader("foo", "bar")
MessageBuilder.withPayload("service.convert")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("aardvark", "bar"))
.build();
this.input.send(message);
assertThat(output.receive(0).getPayload()).isEqualTo("catbar");
assertThat(output.receive(0)).isNull();
}
@Test
public void testvoidOperation() throws Exception {
Message<?> message = MessageBuilder.withPayload("@service.voidOp('foo')").build();
Message<?> message =
MessageBuilder.withPayload("service.voidOp")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("foo"))
.build();
this.input.send(message);
assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue();
}
@@ -130,8 +135,13 @@ public class ControlBusTests {
Map<?, ?> mappings = (Map<?, ?>) result.getPayload();
assertThat(mappings.get("foo")).isEqualTo("bar");
assertThat(mappings.get("baz")).isEqualTo("qux");
messagingTemplate.convertAndSend(input,
"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')");
Properties newMapping = new Properties();
newMapping.setProperty("foo", "qux");
newMapping.setProperty("baz", "bar");
messagingTemplate.send(input,
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
.build());
messagingTemplate.convertAndSend(input, "@'router.handler'.getChannelMappings()");
result = this.output.receive(0);
assertThat(result).isNotNull();
@@ -145,8 +155,8 @@ public class ControlBusTests {
private final CountDownLatch latch = new CountDownLatch(1);
@ManagedOperation
public String convert(String input) {
return "cat";
public String convert(String input, String header) {
return "cat" + header;
}
@ManagedOperation

View File

@@ -89,10 +89,10 @@ import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.interceptor.WireTap;
import org.springframework.integration.config.ControlBusFactoryBean;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.EnableMessageHistory;
import org.springframework.integration.config.EnablePublisher;
import org.springframework.integration.config.ExpressionControlBusFactoryBean;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.config.IntegrationConverter;
import org.springframework.integration.config.SpelFunctionFactoryBean;
@@ -454,15 +454,15 @@ public class EnableIntegrationTests {
assertThat(message.getHeaders().get("foo")).isEqualTo("FOO");
MessagingTemplate messagingTemplate = new MessagingTemplate(this.controlBusChannel);
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
this.controlBusChannel.send(new GenericMessage<>("@pausable.start()"));
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(true);
this.controlBusChannel.send(new GenericMessage<>("@pausable.stop()"));
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
this.controlBusChannel.send(new GenericMessage<>("@pausable.pause()"));
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(false);
this.controlBusChannel.send(new GenericMessage<>("pausable.start"));
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(true);
this.controlBusChannel.send(new GenericMessage<>("pausable.stop"));
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(false);
this.controlBusChannel.send(new GenericMessage<>("pausable.pause"));
Object pausable = this.context.getBean("pausable");
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isTrue();
this.controlBusChannel.send(new GenericMessage<>("@pausable.resume()"));
this.controlBusChannel.send(new GenericMessage<>("pausable.resume"));
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isFalse();
Map<String, ServiceActivatingHandler> beansOfType =
@@ -1113,8 +1113,8 @@ public class EnableIntegrationTests {
@ServiceActivator(inputChannel = "controlBusChannel")
@EndpointId("controlBusEndpoint")
@Role("bar")
public ExpressionControlBusFactoryBean controlBus() {
return new ExpressionControlBusFactoryBean();
public ControlBusFactoryBean controlBus() {
return new ControlBusFactoryBean();
}
@Autowired

View File

@@ -214,7 +214,7 @@ public class IntegrationFlowTests {
.withCauseInstanceOf(MessageDispatchingException.class)
.withMessageContaining("Dispatcher has no subscribers");
this.controlBus.send("@payloadSerializingTransformer.start()");
this.controlBus.send("payloadSerializingTransformer.start");
final AtomicBoolean used = new AtomicBoolean();
@@ -253,7 +253,7 @@ public class IntegrationFlowTests {
.withCauseInstanceOf(MessageDispatchingException.class)
.withMessageContaining("Dispatcher has no subscribers");
this.controlBus.send("@bridge.start()");
this.controlBus.send("bridge.start");
this.bridgeFlow2Input.send(message);
reply = this.bridgeFlow2Output.receive(10000);
assertThat(reply).isNotNull();
@@ -623,7 +623,7 @@ public class IntegrationFlowTests {
@Bean
public IntegrationFlow controlBusFlow() {
return IntegrationFlow.from(ControlBusGateway.class, (gateway) -> gateway.beanName("controlBusGateway"))
.controlBus((endpoint) -> endpoint.id("controlBus"))
.controlBusOnRegistry((endpoint) -> endpoint.id("controlBus"))
.get();
}

View File

@@ -47,8 +47,8 @@ import org.springframework.integration.aop.ReceiveMessageAdvice;
import org.springframework.integration.aop.SimpleActiveIdleReceiveMessageAdvice;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.config.ControlBusFactoryBean;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.ExpressionControlBusFactoryBean;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.scheduling.PollSkipAdvice;
import org.springframework.integration.scheduling.SimplePollSkipStrategy;
@@ -163,9 +163,9 @@ public class PollerAdviceTests {
@Test
public void testSkipSimpleControlBus() {
this.control.send(new GenericMessage<>("@skipper.skipPolls()"));
this.control.send(new GenericMessage<>("skipper.skipPolls"));
assertThat(this.skipper.skipPoll()).isTrue();
this.control.send(new GenericMessage<>("@skipper.reset()"));
this.control.send(new GenericMessage<>("skipper.reset"));
assertThat(this.skipper.skipPoll()).isFalse();
}
@@ -444,8 +444,8 @@ public class PollerAdviceTests {
@Bean
@ServiceActivator(inputChannel = "control")
public ExpressionControlBusFactoryBean controlBus() {
return new ExpressionControlBusFactoryBean();
public ControlBusFactoryBean controlBus() {
return new ControlBusFactoryBean();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2022 the original author or authors.
* Copyright 2015-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,6 +17,7 @@
package org.springframework.integration.support.management;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -26,17 +27,25 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.EnableIntegrationManagement;
import org.springframework.integration.config.IntegrationManagementConfigurer;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.endpoint.AbstractMessageSource;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.handler.ControlBusMessageProcessor;
import org.springframework.integration.router.RecipientListRouter;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -53,7 +62,7 @@ public class IntegrationManagementConfigurerTests {
public void testDefaults() {
DirectChannel channel = new DirectChannel();
AbstractMessageHandler handler = new RecipientListRouter();
AbstractMessageSource<?> source = new AbstractMessageSource<Object>() {
AbstractMessageSource<?> source = new AbstractMessageSource<>() {
@Override
public String getComponentType() {
@@ -94,6 +103,68 @@ public class IntegrationManagementConfigurerTests {
}
}
@Test
public void controlBusIntegration() {
try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ControlBusEagerConfig.class)) {
ControlBusCommandRegistry controlBusCommandRegistry =
ctx.getBean(IntegrationContextUtils.CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME,
ControlBusCommandRegistry.class);
Map<String, Map<ControlBusCommandRegistry.CommandMethod, String>> commands =
controlBusCommandRegistry.getCommands();
assertThat(commands).containsKeys("errorChannel", "nullChannel", "taskScheduler", "channel",
"_org.springframework.integration.errorLogger",
"_org.springframework.integration.errorLogger.handler");
Map<ControlBusCommandRegistry.CommandMethod, String> commandMethodStringMap = commands.get("channel");
List<String> controlBusMethodForChannelBean =
commandMethodStringMap.keySet()
.stream()
.map(ControlBusCommandRegistry.CommandMethod::getMethodName)
.toList();
assertThat(controlBusMethodForChannelBean)
.containsOnly("setLoggingEnabled", "isLoggingEnabled", "setShouldTrack");
Expression isLoggingEnabledCommand =
controlBusCommandRegistry.getExpressionForCommand("nullChannel.isLoggingEnabled");
StandardEvaluationContext evaluationContext = IntegrationContextUtils.getEvaluationContext(ctx);
assertThat(isLoggingEnabledCommand.getValue(evaluationContext, boolean.class)).isFalse();
Expression setLoggingEnabledCommand =
controlBusCommandRegistry.getExpressionForCommand("nullChannel.setLoggingEnabled", boolean.class);
setLoggingEnabledCommand.getValue(evaluationContext, new Object[] {true});
assertThat(isLoggingEnabledCommand.getValue(evaluationContext, boolean.class)).isTrue();
ControlBusMessageProcessor controlBusMessageProcessor = ctx.getBean(ControlBusMessageProcessor.class);
assertThatIllegalArgumentException()
.isThrownBy(() ->
controlBusMessageProcessor.processMessage(new GenericMessage<>("nonSuchBean.command")))
.withMessage("There is no registered bean for requested command: nonSuchBean");
assertThatIllegalArgumentException()
.isThrownBy(() ->
controlBusMessageProcessor.processMessage(new GenericMessage<>("channel.noSuchCommand")))
.withMessageStartingWith("No method 'noSuchCommand' found in bean 'bean 'channel'");
controlBusMessageProcessor.processMessage(
MessageBuilder.withPayload("nullChannel.setLoggingEnabled")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(false))
.build());
assertThat((Boolean) controlBusMessageProcessor.processMessage(
new GenericMessage<>("nullChannel.isLoggingEnabled")))
.isFalse();
}
}
@Configuration
@EnableIntegration
@EnableIntegrationManagement
@@ -113,4 +184,21 @@ public class IntegrationManagementConfigurerTests {
}
@Configuration
@EnableIntegration
@EnableIntegrationManagement(loadControlBusCommands = "true", defaultLoggingEnabled = "false")
public static class ControlBusEagerConfig {
@Bean
MessageChannel channel() {
return new DirectChannel();
}
@Bean
ControlBusMessageProcessor controlBusMessageProcessor(ControlBusCommandRegistry controlBusCommandRegistry) {
return new ControlBusMessageProcessor(controlBusCommandRegistry);
}
}
}