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);
}
}
}

View File

@@ -182,7 +182,7 @@ public class FileTests {
}
assertThat(this.tailChannel.receive(1)).isNull();
this.controlBus.send("@tailer.stop()");
this.controlBus.send("tailer.stop");
while (!tailTestFile.delete()) {
Thread.sleep(100);
@@ -322,7 +322,7 @@ public class FileTests {
@Bean
public IntegrationFlow controlBus() {
return IntegrationFlowDefinition::controlBus;
return IntegrationFlowDefinition::controlBusOnRegistry;
}
@Bean

View File

@@ -268,8 +268,26 @@ class GroovyIntegrationFlowDefinition {
* 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.
* @see org.springframework.integration.handler.ExpressionCommandMessageProcessor* @see GenericEndpointSpec
* @since 6.4
*/
GroovyIntegrationFlowDefinition controlBusOnRegistry(
@DelegatesTo(value = GenericEndpointSpec<ServiceActivatingHandler>, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType.class, options = 'org.springframework.integration.dsl.GenericEndpointSpec')
Closure<?> endpointConfigurer = null) {
this.delegate.controlBusOnRegistry createConfigurerIfAny(endpointConfigurer)
this
}
/**
* 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.
* @see GenericEndpointSpec
* @deprecated in favor of {@link #controlBusOnRegistry}
*/
@Deprecated(since = '6.4', forRemoval = true)
@SuppressWarnings('removal')
GroovyIntegrationFlowDefinition controlBus(
@DelegatesTo(value = GenericEndpointSpec<ServiceActivatingHandler>, strategy = Closure.DELEGATE_FIRST)
@ClosureParams(value = SimpleType.class, options = 'org.springframework.integration.dsl.GenericEndpointSpec')

View File

@@ -47,7 +47,10 @@ import org.springframework.util.ObjectUtils;
* @author Gary Russell
*
* @since 2.0
*
* @deprecated in favor of {@link org.springframework.integration.handler.ControlBusMessageProcessor}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class GroovyCommandMessageProcessor extends AbstractScriptExecutingMessageProcessor<Object>
implements IntegrationPattern {

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.
@@ -30,7 +30,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.integration.config.AbstractSimpleMessageHandlerFactoryBean;
import org.springframework.integration.groovy.GroovyCommandMessageProcessor;
import org.springframework.integration.handler.ServiceActivatingHandler;
import org.springframework.integration.support.management.IntegrationManagedResource;
import org.springframework.jmx.export.annotation.ManagedResource;
@@ -47,8 +46,12 @@ import org.springframework.util.CustomizableThreadCreator;
* @author Artem Bilan
* @author Stefan Reuter
* @author Gary Russell
*
* @since 2.0
*
* @deprecated in favor of {@link org.springframework.integration.config.ControlBusFactoryBean}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class GroovyControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler>
implements BeanClassLoaderAware {
@@ -72,14 +75,16 @@ public class GroovyControlBusFactoryBean extends AbstractSimpleMessageHandlerFac
}
@Override
@SuppressWarnings("removal")
protected MessageHandler createHandler() {
Binding binding = new ManagedBeansBinding(this.getBeanFactory());
GroovyCommandMessageProcessor processor = new GroovyCommandMessageProcessor(binding,
message -> {
Map<String, Object> variables = new HashMap<>();
variables.put("headers", message.getHeaders());
return variables;
});
org.springframework.integration.groovy.GroovyCommandMessageProcessor processor =
new org.springframework.integration.groovy.GroovyCommandMessageProcessor(binding,
message -> {
Map<String, Object> variables = new HashMap<>();
variables.put("headers", message.getHeaders());
return variables;
});
if (this.customizer != null) {
processor.setCustomizer(this.customizer);
}

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.
@@ -28,13 +28,20 @@ import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
*
* @author Dave Syer
* @author Artem Bilan
*
* @since 2.0
*
* @deprecated in favor of {@link org.springframework.integration.config.xml.ControlBusParser}
*/
@Deprecated(since = "6.4", forRemoval = true)
public class GroovyControlBusParser extends AbstractConsumerEndpointParser {
@Override
@SuppressWarnings("removal")
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(GroovyControlBusFactoryBean.class);
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(
org.springframework.integration.groovy.config.GroovyControlBusFactoryBean.class);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "customizer");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");

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,13 +20,16 @@ import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHa
/**
* @author Mark Fisher
* @author Artem Bilan
*
* @since 2.0
*/
public class GroovyNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@SuppressWarnings("removal")
public void init() {
this.registerBeanDefinitionParser("script", new GroovyScriptParser());
this.registerBeanDefinitionParser("control-bus", new GroovyControlBusParser());
registerBeanDefinitionParser("script", new GroovyScriptParser());
registerBeanDefinitionParser("control-bus", new GroovyControlBusParser());
}
}

View File

@@ -69,6 +69,7 @@
<xsd:element name="control-bus">
<xsd:annotation>
<xsd:documentation>
[DEPRECATED in favor 'control-bus' component from core module]
Control bus ('org.springframework.integration.groovy.config.GroovyControlBusFactoryBean') that accepts
messages in the form of Groovy scripts. The scripts should be provided as
String payload in incoming messages. The variable bindings will include any @ManagedResource, Lifecycle,

View File

@@ -1,10 +0,0 @@
def delayHandler = this.'testDelayer.handler'
def delayedMessageCount = delayHandler.delayedMessageCount
println delayedMessageCount
assert 2 == delayedMessageCount
delayHandler.reschedulePersistedMessages()
return true

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:groovy="http://www.springframework.org/schema/integration/groovy"
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/groovy https://www.springframework.org/schema/integration/groovy/spring-integration-groovy.xsd
http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<channel id="controlBusOutput">
<queue/>
</channel>
<channel id="output">
<queue/>
</channel>
<groovy:control-bus input-channel="controlBus" output-channel="controlBusOutput"/>
<beans:bean id="scheduler"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
<beans:property name="phase" value="1073741823"/>
</beans:bean>
<delayer id="testDelayer" input-channel="delayerInput" output-channel="output"
default-delay="1000"
scheduler="scheduler"/>
</beans:beans>

View File

@@ -1,79 +0,0 @@
/*
* 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.
* 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.groovy;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scripting.ScriptSource;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Artem Bilan
*
* @since 2.2
*/
@SpringJUnitConfig
@DirtiesContext
public class GroovyControlBusIntegrationTests {
@Autowired
private MessageChannel controlBus;
@Autowired
private PollableChannel output;
@Autowired
private MessageChannel delayerInput;
@Autowired
ThreadPoolTaskScheduler scheduler;
@Test
public void testDelayerManagement() throws IOException {
Message<String> testMessage = MessageBuilder.withPayload("test").build();
this.delayerInput.send(testMessage);
this.delayerInput.send(testMessage);
this.scheduler.destroy();
// ensure the delayer did not release any messages
assertThat(this.output.receive(500)).isNull();
this.scheduler.afterPropertiesSet();
Resource scriptResource = new ClassPathResource("GroovyControlBusDelayerManagementTest.groovy", getClass());
ScriptSource scriptSource = new ResourceScriptSource(scriptResource);
Message<?> message = MessageBuilder.withPayload(scriptSource.getScriptAsString()).build();
this.controlBus.send(message);
assertThat(this.output.receive(10000)).isNotNull();
assertThat(this.output.receive(10000)).isNotNull();
}
}

View File

@@ -1,119 +0,0 @@
/*
* 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.
* 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.groovy;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import groovy.lang.Binding;
import groovy.lang.MissingPropertyException;
import org.junit.jupiter.api.Test;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.integration.scripting.DefaultScriptVariableGenerator;
import org.springframework.integration.scripting.ScriptVariableGenerator;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Dave Syer
* @author Artem Bilan
* @author Gunnar Hillert
*
* @since 2.0
*/
public class GroovyScriptPayloadMessageProcessorTests {
private final AtomicInteger countHolder = new AtomicInteger();
private GroovyCommandMessageProcessor processor;
@Test
public void testSimpleExecution() {
int count = countHolder.getAndIncrement();
Message<?> message = MessageBuilder.withPayload("headers.foo" + count).setHeader("foo" + count, "bar").build();
processor = new GroovyCommandMessageProcessor();
Object result = processor.processMessage(message);
assertThat(result.toString()).isEqualTo("bar");
}
@Test
public void testDoubleExecutionWithNewScript() {
processor = new GroovyCommandMessageProcessor();
Message<?> message = MessageBuilder.withPayload("headers.foo").setHeader("foo", "bar").build();
Object result = processor.processMessage(message);
assertThat(result.toString()).isEqualTo("bar");
message = MessageBuilder.withPayload("headers.bar").setHeader("bar", "spam").build();
result = processor.processMessage(message);
assertThat(result.toString()).isEqualTo("spam");
}
@Test
public void testSimpleExecutionWithContext() {
Message<?> message = MessageBuilder.withPayload("\"spam is $spam foo is $headers.foo\"")
.setHeader("foo", "bar").build();
ScriptVariableGenerator scriptVariableGenerator =
new DefaultScriptVariableGenerator(Collections.singletonMap("spam", "bucket"));
MessageProcessor<Object> processor = new GroovyCommandMessageProcessor(scriptVariableGenerator);
Object result = processor.processMessage(message);
assertThat(result.toString()).isEqualTo("spam is bucket foo is bar");
}
@Test
public void testBindingOverwrite() {
Binding binding = new Binding() {
@Override
public Object getVariable(String name) {
throw new RuntimeException("intentional");
}
};
Message<?> message = MessageBuilder.withPayload("foo").build();
processor = new GroovyCommandMessageProcessor(binding);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> processor.processMessage(message))
.withMessage("intentional");
}
@Test
public void testBindingOverwriteWithContext() {
final String defaultValue = "default";
Binding binding = new Binding() {
@Override
public Object getVariable(String name) {
try {
return super.getVariable(name);
}
catch (MissingPropertyException e) {
// ignore
}
return defaultValue;
}
};
ScriptVariableGenerator scriptVariableGenerator =
new DefaultScriptVariableGenerator(Collections.singletonMap("spam", "bucket"));
Message<?> message = MessageBuilder.withPayload("\"spam is $spam, foo is $foo\"").build();
processor = new GroovyCommandMessageProcessor(binding, scriptVariableGenerator);
Object result = processor.processMessage(message);
assertThat(result.toString()).isEqualTo("spam is bucket, foo is default");
}
}

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:groovy="http://www.springframework.org/schema/integration/groovy"
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/integration/groovy https://www.springframework.org/schema/integration/groovy/spring-integration-groovy.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<channel id="output">
<queue/>
</channel>
<beans:bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<beans:property name="scopes">
<beans:map>
<beans:entry key="thread" value="org.springframework.context.support.SimpleThreadScope"/>
<beans:entry key="request" value="org.springframework.web.context.request.RequestScope"/>
</beans:map>
</beans:property>
</beans:bean>
<groovy:control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true"
customizer="groovyCustomizer">
<groovy:request-handler-advice-chain>
<beans:bean class="org.springframework.integration.groovy.config.GroovyControlBusTests$FooAdvice" />
</groovy:request-handler-advice-chain>
</groovy:control-bus>
<beans:bean id="service" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
<beans:bean id="threadScopedService"
scope="thread"
class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
<beans:bean id="requestScopedService"
scope="request"
class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
<beans:bean id="nonManagedService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$NonManagedService" />
<beans:bean id="abstractService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service"
abstract="true"/>
<beans:bean id="prototypeService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service"
scope="prototype"/>
<beans:bean id="groovyCustomizer"
class="org.springframework.integration.groovy.config.GroovyControlBusTests$MyGroovyCustomizer"/>
</beans:beans>

View File

@@ -1,258 +0,0 @@
/*
* 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.
* 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.groovy.config;
import java.util.HashMap;
import java.util.Map;
import groovy.lang.GroovyObject;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.BeanIsAbstractException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.scripting.groovy.GroovyObjectCustomizer;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/**
* @author Dave Syer
* @author Artem Bilan
* @author Gary Russell
* @author Gunnar Hillert
*
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class GroovyControlBusTests {
@Autowired
private MessageChannel input;
@Autowired
private PollableChannel output;
@Autowired
private MyGroovyCustomizer groovyCustomizer;
private static volatile int adviceCalled;
@Before
public void beforeTest() {
adviceCalled = 0;
}
@Test
public void testOperationOfControlBus() { // long is > 3
this.groovyCustomizer.executed = false;
Message<?> message =
MessageBuilder.withPayload(
"def result = service.convert('aardvark'); def foo = headers.foo; result+foo")
.setHeader("foo", "bar")
.build();
this.input.send(message);
assertThat(output.receive(0).getPayload()).isEqualTo("catbar");
assertThat(output.receive(0)).isNull();
assertThat(this.groovyCustomizer.executed).isTrue();
assertThat(adviceCalled).isEqualTo(1);
}
@Test //INT-2567
public void testOperationWithCustomScope() {
Message<?> message =
MessageBuilder.withPayload("def result = threadScopedService.convert('testString')")
.build();
this.input.send(message);
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
}
@Test //INT-2567
public void testFailOperationWithCustomScope() {
try {
Message<?> message =
MessageBuilder.withPayload("def result = requestScopedService.convert('testString')")
.build();
this.input.send(message);
fail("Expected BeanCreationException");
}
catch (Exception e) {
Throwable cause = e.getCause();
assertThat(cause instanceof BeanCreationException)
.as("Expected BeanCreationException, got " + cause.getClass() + ":" + cause
.getMessage()).isTrue();
assertThat(cause.getMessage().contains("requestScopedService")).isTrue();
}
}
@Test //INT-2567
public void testOperationWithRequestCustomScope() {
RequestContextHolder.setRequestAttributes(new MockRequestAttributes());
Message<?> message =
MessageBuilder.withPayload("def result = requestScopedService.convert('testString')")
.build();
this.input.send(message);
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
RequestContextHolder.resetRequestAttributes();
}
@Test //INT-2567
public void testFailOperationOnNonManagedComponent() {
try {
Message<?> message =
MessageBuilder.withPayload("def result = nonManagedService.convert('testString')")
.build();
this.input.send(message);
fail("Expected BeanCreationNotAllowedException");
}
catch (MessageHandlingException e) {
Throwable cause = e.getCause();
assertThat(cause instanceof BeanCreationNotAllowedException)
.as("Expected BeanCreationNotAllowedException, got " + cause.getClass() + ":" + cause.getMessage())
.isTrue();
assertThat(cause.getMessage().contains("nonManagedService")).isTrue();
}
}
@Test //INT-2631
public void testFailOperationOnAbstractBean() {
try {
Message<?> message = MessageBuilder.withPayload("abstractService.convert('testString')").build();
this.input.send(message);
fail("Expected BeanIsAbstractException");
}
catch (MessageHandlingException e) {
Throwable cause = e.getCause();
assertThat(cause instanceof BeanIsAbstractException)
.as("Expected BeanIsAbstractException, got " + cause.getClass() + ":" + cause.getMessage())
.isTrue();
assertThat(cause.getMessage().contains("abstractService")).isTrue();
}
}
@Test //INT-2631
public void testOperationOnPrototypeBean() {
Message<?> message = MessageBuilder.withPayload("def result = prototypeService.convert('testString')").build();
this.input.send(message);
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
}
@ManagedResource
public static class Service {
@ManagedOperation
public String convert(String input) {
return "cat";
}
}
public static class NonManagedService {
public String convert(String input) {
return "cat";
}
}
public static class MyGroovyCustomizer implements GroovyObjectCustomizer {
private volatile boolean executed;
@Override
public void customize(GroovyObject goo) {
this.executed = true;
}
}
private static class MockRequestAttributes implements RequestAttributes {
private final Map<String, Object> fakeRequest = new HashMap<>();
@Override
public Object getAttribute(String name, int scope) {
return fakeRequest.get(name);
}
@Override
public void setAttribute(String name, Object value, int scope) {
fakeRequest.put(name, value);
}
@Override
public void removeAttribute(String name, int scope) {
}
@Override
public String[] getAttributeNames(int scope) {
return null;
}
@Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
}
@Override
public Object resolveReference(String key) {
return null;
}
@Override
public String getSessionId() {
return null;
}
@Override
public Object getSessionMutex() {
return null;
}
}
public static class FooAdvice extends AbstractRequestHandlerAdvice {
@Override
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
adviceCalled++;
return callback.execute();
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.http.config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.integration.http.management.ControlBusController;
import org.springframework.integration.support.management.ControlBusCommandRegistry;
/**
* Registers the {@link ControlBusController} bean.
* <p>
* Also calls {@link ControlBusCommandRegistry#setEagerInitialization(boolean)} with {@code true}
* to load all the available commands in the application context.
*
* @author Artem Bilan
*
* @since 6.4
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ControlBusControllerConfiguration {
private static final Log LOGGER = LogFactory.getLog(IntegrationGraphControllerRegistrar.class);
@Bean
ControlBusController controlBusController(ControlBusCommandRegistry controlBusCommandRegistry,
@Qualifier("mvcConversionService") FormattingConversionService conversionService) {
if (!HttpContextUtils.WEB_MVC_PRESENT && !HttpContextUtils.WEB_FLUX_PRESENT) {
LOGGER.warn("The 'IntegrationGraphController' isn't registered with the application context because" +
" there is no 'org.springframework.web.servlet.DispatcherServlet' or" +
" 'org.springframework.web.reactive.DispatcherHandler' in the classpath.");
return null;
}
controlBusCommandRegistry.setEagerInitialization(true);
return new ControlBusController(controlBusCommandRegistry, conversionService);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2016-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.http.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Enables the
* {@link org.springframework.integration.http.management.ControlBusController} if
* {@code org.springframework.web.servlet.DispatcherServlet} or
* {@code org.springframework.web.reactive.DispatcherHandler} is present in the classpath.
*
* @author Artem Bilan
*
* @since 6.4
*
* @see org.springframework.integration.http.management.ControlBusController
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Import(ControlBusControllerConfiguration.class)
public @interface EnableControlBusController {
}

View File

@@ -0,0 +1,149 @@
/*
* 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.http.management;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.support.management.ControlBusCommandRegistry;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* The REST Controller to provide the management API for Control Bus pattern.
*
* @author Artem Bilan
*
* @since 4.3
*/
@RestController
@RequestMapping("/control-bus")
public class ControlBusController implements BeanFactoryAware, InitializingBean {
private final ControlBusCommandRegistry controlBusCommandRegistry;
private final FormattingConversionService conversionService;
private BeanFactory beanFactory;
private EvaluationContext evaluationContext;
public ControlBusController(ControlBusCommandRegistry controlBusCommandRegistry,
@Qualifier("mvcConversionService") FormattingConversionService conversionService) {
this.controlBusCommandRegistry = controlBusCommandRegistry;
this.conversionService = conversionService;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
this.evaluationContext = IntegrationContextUtils.getEvaluationContext(this.beanFactory);
}
@GetMapping(name = "getCommands")
public List<ControlBusBean> getCommands() {
return this.controlBusCommandRegistry.getCommands()
.entrySet()
.stream()
.map((beanEntry) -> createControlBusBean(beanEntry.getKey(), beanEntry.getValue()))
.toList();
}
@GetMapping(name = "getCommandsForBean", path = "/{beanName}")
public ControlBusBean getCommandsForBean(@PathVariable String beanName) {
Map<ControlBusCommandRegistry.CommandMethod, String> commandsForBean =
this.controlBusCommandRegistry.getCommands()
.get(beanName);
return createControlBusBean(beanName, commandsForBean);
}
private ControlBusBean createControlBusBean(String beanName,
Map<ControlBusCommandRegistry.CommandMethod, String> commandsForBean) {
List<ControlBusCommand> commands =
commandsForBean.keySet()
.stream()
.map(this::converControlBusCommand)
.toList();
return new ControlBusBean(beanName, commands);
}
private ControlBusCommand converControlBusCommand(ControlBusCommandRegistry.CommandMethod commandMethod) {
return new ControlBusCommand(commandMethod.getBeanName() + '.' + commandMethod.getMethodName(),
commandMethod.getDescription(),
Arrays.asList(commandMethod.getParameterTypes()));
}
@PostMapping(name = "invokeCommand", path = "/{command}")
public Object invokeCommand(@PathVariable String command,
@RequestBody(required = false) List<CommandArgument> arguments) {
Class<?>[] parameterTypes = new Class<?>[0];
if (!CollectionUtils.isEmpty(arguments)) {
parameterTypes = arguments.stream()
.map(CommandArgument::parameterType)
.toArray(Class<?>[]::new);
}
Expression commandExpression = this.controlBusCommandRegistry.getExpressionForCommand(command, parameterTypes);
Object[] parameterValues = null;
if (!CollectionUtils.isEmpty(arguments)) {
parameterValues = arguments.stream()
.map((arg) -> this.conversionService.convert(arg.value, arg.parameterType))
.toArray(Object[]::new);
}
return commandExpression.getValue(this.evaluationContext, parameterValues);
}
public record ControlBusBean(String beanName, List<ControlBusCommand> commands) {
}
public record ControlBusCommand(String command, String description, List<Class<?>> parameterTypes) {
}
public record CommandArgument(String value, Class<?> parameterType) {
}
}

View File

@@ -0,0 +1,163 @@
/*
* 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.http.management;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.http.config.EnableControlBusController;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.handler;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Artem Bilan
*
* @since 6.4
*/
@SpringJUnitWebConfig
@DirtiesContext
public class ControlBusControllerTests {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@Autowired
TestManagementComponent testManagementComponent;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
void allCommandsAreRegistered() throws Exception {
this.mockMvc.perform(get("/control-bus")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(handler().handlerType(ControlBusController.class))
.andExpect(handler().methodName("getCommands"))
.andExpect(content().string(Matchers.containsString("testManagementComponent.operation")))
.andExpect(content().string(Matchers.containsString("testManagementComponent.operation2")))
.andExpect(content().string(Matchers.containsString("taskScheduler.setPoolSize")))
.andExpect(content().string(Matchers.containsString("integrationHeaderChannelRegistry.runReaper")))
.andExpect(content().string(Matchers.containsString("_org.springframework.integration.errorLogger.isRunning")))
.andExpect(content().string(Matchers.containsString("The overloaded operation with int argument")))
.andExpect(content().string(Matchers.containsString("The overloaded operation with two arguments")));
}
@Test
void commandsForBean() throws Exception {
this.mockMvc.perform(get("/control-bus/testManagementComponent")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(handler().handlerType(ControlBusController.class))
.andExpect(handler().methodName("getCommandsForBean"))
.andExpect(content().string(Matchers.containsString("testManagementComponent.operation")))
.andExpect(content().string(Matchers.containsString("testManagementComponent.operation2")));
}
@Test
void controlBusCommandIsPerformedOverRestCall() throws Exception {
this.mockMvc.perform(post("/control-bus/testManagementComponent.operation")
.contentType(MediaType.APPLICATION_JSON)
.content("""
[
{
"value": "1",
"parameterType": "int"
}
]
"""))
.andExpect(status().isOk())
.andExpect(handler().handlerType(ControlBusController.class))
.andExpect(handler().methodName("invokeCommand"));
verify(this.testManagementComponent).operation(eq(1));
this.mockMvc.perform(post("/control-bus/testManagementComponent.operation2"))
.andExpect(status().isOk())
.andExpect(handler().handlerType(ControlBusController.class))
.andExpect(handler().methodName("invokeCommand"))
.andExpect(content().string("123"));
verify(this.testManagementComponent).operation2();
}
@Configuration
@EnableWebMvc
@EnableIntegration
@EnableControlBusController
static class ContextConfiguration {
@Bean
TestManagementComponent testManagementComponent() {
return spy(new TestManagementComponent());
}
}
@ManagedResource
private static class TestManagementComponent {
@ManagedOperation
public void operation() {
}
@ManagedOperation(description = "The overloaded operation with int argument")
public void operation(int input) {
}
@ManagedOperation(description = "The overloaded operation with two arguments")
public void operation(int input1, String input2) {
}
@ManagedOperation
public int operation2() {
return 123;
}
}
}

View File

@@ -45,7 +45,7 @@
<int:channel id="cbChannel" />
<int:control-bus input-channel="cbChannel" />
<int:control-bus input-channel="cbChannel" use-registry="true"/>
<int:gateway default-request-channel="cbChannel"
service-interface="org.springframework.integration.ip.tcp.ClientModeControlBusTests$ControlBus"/>

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.
@@ -33,8 +33,8 @@ import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.QueueChannel;
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.core.MessagingTemplate;
import org.springframework.integration.jdbc.storedproc.PrimeMapper;
@@ -80,9 +80,9 @@ public class StoredProcJavaConfigTests {
// verify maxMessagesPerPoll == 1
assertThat(received).isNull();
MessagingTemplate template = new MessagingTemplate(this.control);
template.convertAndSend("@'storedProc.inboundChannelAdapter'.stop()");
template.convertAndSend("'storedProc.inboundChannelAdapter'.stop");
assertThat(template.convertSendAndReceive(
"@'storedProc.inboundChannelAdapter'.isRunning()", Boolean.class))
"'storedProc.inboundChannelAdapter'.isRunning", Boolean.class))
.isFalse();
}
@@ -97,8 +97,8 @@ public class StoredProcJavaConfigTests {
@Bean
@ServiceActivator(inputChannel = "control")
public ExpressionControlBusFactoryBean controlBus() {
return new ExpressionControlBusFactoryBean();
public ControlBusFactoryBean controlBus() {
return new ControlBusFactoryBean();
}
@Bean

View File

@@ -167,14 +167,14 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Test
public void testPollingFlow() {
this.controlBus.send("@'integerMessageSource.inboundChannelAdapter'.start()");
this.controlBus.send("'integerMessageSource.inboundChannelAdapter'.start");
assertThat(this.beanFactory.getBean("integerChannel")).isInstanceOf(FixedSubscriberChannel.class);
for (int i = 0; i < 5; i++) {
Message<?> message = this.outputChannel.receive(20000);
assertThat(message).isNotNull();
assertThat(message.getPayload()).isEqualTo("" + i);
}
this.controlBus.send("@'integerMessageSource.inboundChannelAdapter'.stop()");
this.controlBus.send("'integerMessageSource.inboundChannelAdapter'.stop");
assertThat(((InterceptableChannel) this.outputChannel).getInterceptors())
.contains(this.testChannelInterceptor);
@@ -358,7 +358,7 @@ public class JmsTests extends ActiveMQMultiContextTests {
@Bean
public IntegrationFlow controlBus() {
return IntegrationFlowDefinition::controlBus;
return IntegrationFlowDefinition::controlBusOnRegistry;
}
@Bean

View File

@@ -21,7 +21,7 @@
<int:channel id="control" />
<int:control-bus input-channel="control" />
<int:control-bus input-channel="control" use-registry="true"/>
<int:channel id="qux">
<int:queue />

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,36 +17,37 @@
package org.springframework.integration.jmx;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
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 Gary Russell
* @author Artem Bilan
*
* @since 2.1
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class UpdateMappingsTests {
@@ -64,7 +65,10 @@ public class UpdateMappingsTests {
@Test
public void test() {
control.send(new GenericMessage<String>("@myRouter.setChannelMapping('baz', 'qux')"));
control.send(
MessageBuilder.withPayload("myRouter.setChannelMapping")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("baz", "qux"))
.build());
Message<?> message = MessageBuilder.withPayload("Hello, world!")
.setHeader("routing.header", "baz").build();
in.send(message);
@@ -75,15 +79,28 @@ public class UpdateMappingsTests {
public void testChangeRouterMappings() {
MessagingTemplate messagingTemplate = new MessagingTemplate();
messagingTemplate.setReceiveTimeout(1000);
messagingTemplate.convertAndSend(control,
"@'router.handler'.replaceChannelMappings('foo=bar \n baz=qux')");
Map<?, ?> mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
Properties newMapping = new Properties();
newMapping.setProperty("foo", "bar");
newMapping.setProperty("baz", "qux");
messagingTemplate.send(control,
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
.build());
Map<?, ?> mappings =
messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
assertThat(mappings).isNotNull();
assertThat(mappings.size()).isEqualTo(2);
assertThat(mappings.get("foo")).isEqualTo("bar");
assertThat(mappings.get("baz")).isEqualTo("qux");
messagingTemplate.convertAndSend(control,
"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')");
newMapping = new Properties();
newMapping.setProperty("foo", "qux");
newMapping.setProperty("baz", "bar");
messagingTemplate
.send(control,
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
.build());
mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
assertThat(mappings.size()).isEqualTo(2);
assertThat(mappings.get("baz")).isEqualTo("bar");
@@ -98,13 +115,14 @@ public class UpdateMappingsTests {
.getInstance("update.mapping.domain:type=MessageHandler,name=router,bean=endpoint"),
null);
assertThat(names.size()).isEqualTo(1);
Map<String, String> map = new HashMap<String, String>();
Map<String, String> map = new HashMap<>();
map.put("foo", "bar");
map.put("baz", "qux");
Object[] params = new Object[] {map};
this.server.invoke(names.iterator().next(), "setChannelMappings", params,
new String[] {"java.util.Map"});
Map<?, ?> mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
Map<?, ?> mappings =
messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
assertThat(mappings).isNotNull();
assertThat(mappings.size()).isEqualTo(2);
assertThat(mappings.get("foo")).isEqualTo("bar");

View File

@@ -16,7 +16,7 @@
<si:channel id="controlChannel"/>
<si:control-bus id = "cb" input-channel="controlChannel"/>
<si:control-bus id = "cb" input-channel="controlChannel" use-registry="true"/>
<jmx:mbean-export id="integrationMbeanExporter" server="mbs" default-domain="tests.ControlBusParser"/>

View File

@@ -14,7 +14,7 @@
<int:channel id="toControlBus" />
<int:control-bus input-channel="toControlBus" />
<int:control-bus input-channel="toControlBus" use-registry="true"/>
<int-jmx:mbean-export id="integrationMbeanExporter"
default-domain="self-destruct" />

View File

@@ -17,6 +17,6 @@
selector-expression="payload.contains('stop')" />
</int:recipient-list-router>
<int:control-bus input-channel="controlBusChannel" />
<int:control-bus input-channel="controlBusChannel" use-registry="true"/>
</beans>

View File

@@ -20,8 +20,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.jpa.support.JpaParameter;
import org.springframework.integration.util.AbstractExpressionEvaluator;
import org.springframework.lang.Nullable;
@@ -72,7 +72,7 @@ final class ExpressionEvaluatingParameterSourceUtils {
public static class ParameterExpressionEvaluator extends AbstractExpressionEvaluator {
@Override
public StandardEvaluationContext getEvaluationContext() { // NOSONAR - not useless, increases visibility
public EvaluationContext getEvaluationContext() { // NOSONAR - not useless, increases visibility
return super.getEvaluationContext();
}

View File

@@ -159,6 +159,7 @@
** xref:http/proxy.adoc[]
** xref:http/header-mapping.adoc[]
** xref:http/int-graph-controller.adoc[]
** xref:http/control-bus-controller.adoc[]
** xref:http/samples.adoc[]
* xref:ip.adoc[]
** xref:ip/intro.adoc[]

View File

@@ -71,7 +71,7 @@ See xref:claim-check.adoc[Claim Check] for more details.
We have provided implementations of the https://www.enterpriseintegrationpatterns.com/ControlBus.html[control bus] pattern, which lets you use messaging to manage and monitor endpoints and channels.
The implementations include both a SpEL-based approach and one that runs Groovy scripts.
See xref:groovy.adoc#groovy-control-bus[Control Bus] and xref:groovy.adoc#groovy-control-bus[Control Bus] for more details.
See xref:control-bus.adoc[Control Bus] for more details.
[[new-adapters]]
== New Channel Adapters and Gateways

View File

@@ -66,7 +66,7 @@ The `splitter` now supports a `discardChannel` configuration option.
See xref:splitter.adoc[Splitter] for more information.
The Control Bus can now handle `Pausable` (extension of `Lifecycle`) operations.
See xref:groovy.adoc#groovy-control-bus[Control Bus] for more information.
See xref:control-bus.adoc[Control Bus] for more information.
The `Function<MessageGroup, Map<String, Object>>` strategy has been introduced for the aggregator component to merge and compute headers for output messages.
See xref:aggregator.adoc#aggregator-api[Aggregator Programming Model] for more information.

View File

@@ -120,7 +120,7 @@ The `MessageHandler` instances (`MessageSource` instances) are also eligible to
Starting with version 4.0, all messaging annotations provide `SmartLifecycle` options (`autoStartup` and `phase`) to allow endpoint lifecycle control on application context initialization.
They default to `true` and `0`, respectively.
To change the state of an endpoint (such as `start()` or `stop()`), you can obtain a reference to the endpoint bean by using the `BeanFactory` (or autowiring) and invoke the methods.
Alternatively, you can send a command message to the `Control Bus` (see xref:groovy.adoc#groovy-control-bus[Control Bus]).
Alternatively, you can send a command message to the xref:control-bus.adoc[Control Bus].
For these purposes, you should use the `beanName` mentioned earlier in the preceding paragraph.
[IMPORTANT]

View File

@@ -42,7 +42,7 @@ By default, only `MessageHeaders.ID` and `MessageHeaders.TIMESTAMP` are not copi
Since version 4.3.2.
<7> A comma-separated list of `AbstractEndpoint` bean names patterns (`xxx*`, `*xxx`, `*xxx*` or `xxx*yyy`) that should not be started automatically during application startup.
You can manually start these endpoints later by their bean name through a `Control Bus` (see xref:groovy.adoc#groovy-control-bus[Control Bus]), by their role with the `SmartLifecycleRoleController` (see xref:endpoint.adoc#endpoint-roles[Endpoint Roles]), or by `Lifecycle` bean injection.
You can manually start these endpoints later by their bean name through a xref:control-bus.adoc[Control Bus], by their role with the `SmartLifecycleRoleController` (see xref:endpoint-roles.adoc[Endpoint Roles]), or by `Lifecycle` bean injection.
You can explicitly override the effect of this global property by specifying `auto-startup` XML annotation or the `autoStartup` annotation attribute or by calling `AbstractEndpoint.setAutoStartup()` in the bean definition.
Since version 4.3.12.

View File

@@ -199,7 +199,7 @@ These methods can be invoked directly by getting a reference to the registry, or
[source]
----
"@integrationHeaderChannelRegistry.runReaper()"
"integrationHeaderChannelRegistry.runReaper"
----
This sub-element is a convenience, and is the equivalent of specifying the following configuration:

View File

@@ -4,6 +4,8 @@
As described in the https://www.enterpriseintegrationpatterns.com/[_Enterprise Integration Patterns_] (EIP) book, the idea behind the control bus is that the same messaging system can be used for monitoring and managing the components within the framework as is used for "`application-level`" messaging.
In Spring Integration, we build upon the adapters described above so that you can send messages as a means of invoking exposed operations.
IMPORTANT: Since Control Bus is powerful enough to make changes into the system state, it is recommended to secure its messages reception (see `SecurityContextChannelInterceptor`) and expose Control Bus management (message source) only into DMZ.
The following example shows how to configure a control bus with XML:
[source,xml]
@@ -15,33 +17,84 @@ The control bus has an input channel that can be accessed for invoking operation
It also has all the common properties of a service activating endpoint.
For example, you can specify an output channel if the result of the operation has a return value that you want to send on to a downstream channel.
The control bus runs messages on the input channel as Spring Expression Language (SpEL) expressions.
It takes a message, compiles the body to an expression, adds some context, and then runs it.
The default context supports any method that has been annotated with `@ManagedAttribute` or `@ManagedOperation`.
It also supports the methods on Spring's `Lifecycle` interface (and its `Pausable` extension since version 5.2), and it supports methods that are used to configure several of Spring's `TaskExecutor` and `TaskScheduler` implementations.
The control bus runs messages on the input channel as a managed operation in a simple string format like `beanName.methodName`.
The arguments for the target method parameters must be supplied as a list in the `IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS` header.
The bean and the method to call is resolved from the `ControlBusCommandRegistry` infrastructure bean.
By default, the `ControlBusCommandRegistry` registers commands on demand: its `eagerInitialization` flag can be turned on via `@EnableIntegrationManagement(loadControlBusCommands = "true")`.
The functionality of Control Bus is similar to JMX, therefore method eligibility for command must honor these requirements:
- The method that has been annotated with `@ManagedAttribute` or `@ManagedOperation`;
- Spring's `Lifecycle` interface (and its `Pausable` extension since version 5.2);
- The methods that are used to configure several of Spring's `TaskExecutor` and `TaskScheduler` implementations.
The simplest way to ensure that your own methods are available to the control bus is to use the `@ManagedAttribute` or `@ManagedOperation` annotations.
Since those annotations are also used for exposing methods to a JMX MBean registry, they offer a convenient by-product: Often, the same types of operations you want to expose to the control bus are reasonable for exposing through JMX).
Resolution of any particular instance within the application context is achieved in the typical SpEL syntax.
To do so, provide the bean name with the SpEL prefix for beans (`@`).
For example, to execute a method on a Spring Bean, a client could send a message to the operation channel as follows:
Since those annotations are also used for exposing methods to a JMX MBean registry, they offer a convenient by-product: often, the same types of operations you want to expose to the control bus are reasonable for exposing through JMX).
See more information in the `ControlBusCommandRegistry` and `ControlBusMethodFilter` Javadocs.
To execute a method on a Spring Bean, a client could send a message to the operation channel as follows:
[source,java]
----
Message operation = MessageBuilder.withPayload("@myServiceBean.shutdown()").build();
operationChannel.send(operation)
Message<?> operation = MessageBuilder.withPayload("myServiceBean.shutdown").build();
operationChannel.send(operation);
----
The root of the context for the expression is the `Message` itself, so you also have access to the `payload` and `headers` as variables within your expression.
This is consistent with all the other expression support in Spring Integration endpoints.
If target method to call has arguments (e.g. `ThreadPoolTaskExecutor.setMaxPoolSize(int maxPoolSize)`), those values has to be provided as `IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS` header:
With Java annotations, you can configured the control bus as follows:
[source,java]
----
Message<?> operation =
MessageBuilder.withPayload("myTaskExecutor.setMaxPoolSize")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(10))
.build();
operationChannel.send(operation);
----
You can think about these commands as `PreparedStatement` instances in JDBC with parameter binding.
The types of arguments must match types of method parameters.
They are used as additional criteria to select a method to call according to Java method overloading feature.
For example the component:
[source,java]
----
@ManagedResource
class TestManagementComponent {
@ManagedOperation
public void operation() {
}
@ManagedOperation(description = "The overloaded operation with int argument")
public void operation(int input) {
}
@ManagedOperation(description = "The overloaded operation with two arguments")
public void operation(int input1, String input2) {
}
@ManagedOperation
public int operation2() {
return 123;
}
}
----
will expose 3 commands with `operation` name.
When we call `testManagementComponent.operation` command, we should choose a proper list of values for the `IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS` header to let the `ControlBusCommandRegistry` to filter out the target method on the bean.
With Java annotations, you can configure the control bus as follows:
[source,java]
----
@Bean
@ServiceActivator(inputChannel = "operationChannel")
public ExpressionControlBusFactoryBean controlBus() {
return new ExpressionControlBusFactoryBean();
public ControlBusFactoryBean controlBus() {
return new ControlBusFactoryBean();
}
----
@@ -68,3 +121,5 @@ public IntegrationFlow controlBus() {
----
In this case, the channel is named `controlBus.input`.
Also, see xref:http/control-bus-controller.adoc[Control Bus REST Controller] for exposing Control Bus management over HTTP.

View File

@@ -192,7 +192,7 @@ These operations can be invoked through a `Control Bus` command, as the followin
[source,java]
----
Message<String> delayerReschedulingMessage =
MessageBuilder.withPayload("@'delayer.handler'.reschedulePersistedMessages()").build();
MessageBuilder.withPayload("'delayer.handler'.reschedulePersistedMessages").build();
controlBusChannel.send(delayerReschedulingMessage);
----

View File

@@ -129,7 +129,7 @@ This means that the poller continues calling `receive()` without waiting, until
For example, if a poller has a ten-second interval trigger and a `maxMessagesPerPoll` setting of `25`, and it is polling a channel that has 100 messages in its queue, all 100 messages can be retrieved within 40 seconds.
It grabs 25, waits ten seconds, grabs the next 25, and so on.
If `maxMessagesPerPoll` is configured with a negative value, then `MessageSource.receive()` is called within a single polling cycle until it returns `null`.
Starting with version 5.5, a `0` value has a special meaning - skip the `MessageSource.receive()` call altogether, which may be considered as pausing for this polling endpoint until the `maxMessagesPerPoll` is changed to a n non-zero value at a later time, e.g. via a Control Bus.
Starting with version 5.5, a `0` value has a special meaning - skip the `MessageSource.receive()` call altogether, which may be considered as pausing for this polling endpoint until the `maxMessagesPerPoll` is changed to a non-zero value at a later time, e.g. via a Control Bus.
The `receiveTimeout` property specifies the amount of time the poller should wait if no messages are available when it invokes the receive operation.
For example, consider two options that seem similar on the surface but are actually quite different: The first has an interval trigger of 5 seconds and a receive timeout of 50 milliseconds, while the second has an interval trigger of 50 milliseconds and a receive timeout of 5 seconds.

View File

@@ -18,7 +18,7 @@ When all files are consumed, the remote fetch is attempted again, to pick up any
IMPORTANT: When you deploy multiple instances of an application, we recommend a small `max-fetch-size`, to avoid one instance "`grabbing`" all the files and starving other instances.
Another use for `max-fetch-size` is if you want to stop fetching remote files but continue to process files that have already been fetched.
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:groovy.adoc#groovy-control-bus[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:control-bus.adoc[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
If the poller is active when the property is changed, the change takes effect on the next poll.
Starting with version 5.1, the synchronizer can be provided with a `Comparator<FTPFile>`.

View File

@@ -139,37 +139,3 @@ NOTE: Using `compilerConfiguration` does not automatically add an `ASTTransforma
If you still need `CompileStatic`, you should manually add a `new ASTTransformationCustomizer(CompileStatic.class)` into the `CompilationCustomizers` of that custom `compilerConfiguration`.
NOTE: The Groovy compiler customization does not have any effect on the `refresh-check-delay` option, and reloadable scripts can be statically compiled, too.
[[groovy-control-bus]]
== Control Bus
As described in (https://www.enterpriseintegrationpatterns.com/ControlBus.html[Enterprise Integration Patterns]), the idea behind the control bus is that you can use the same messaging system for monitoring and managing the components within the framework as is used for "`application-level`" messaging.
In Spring Integration, we build upon the adapters described earlier so that you can send Messages as a means of invoking exposed operations.
One option for those operations is Groovy scripts.
The following example configures a Groovy script for the control bus:
[source,xml]
----
<int-groovy:control-bus input-channel="operationChannel"/>
----
The control bus has an input channel that can be accessed to invoke operations on the beans in the application context.
The Groovy control bus runs messages on the input channel as Groovy scripts.
It takes a message, compiles the body to a script, customizes it with a `GroovyObjectCustomizer`, and runs it.
The control bus' `MessageProcessor` exposes all beans in the application context that are annotated with `@ManagedResource` and implement Spring's `Lifecycle` interface or extend Spring's `CustomizableThreadCreator` base class (for example, several of the `TaskExecutor` and `TaskScheduler` implementations).
IMPORTANT: Be careful about using managed beans with custom scopes (such as 'request') in the Control Bus' command scripts, especially inside an asynchronous message flow.
If `MessageProcessor` of the control bus cannot expose a bean from the application context, you may end up with some `BeansException` during the command script's run.
For example, if a custom scope's context is not established, the attempt to get a bean within that scope triggers a `BeanCreationException`.
If you need to further customize the Groovy objects, you can also provide a reference to a bean that implements `GroovyObjectCustomizer` through the `customizer` attribute, as the following example shows:
[source,xml]
----
<int-groovy:control-bus input-channel="input"
output-channel="output"
customizer="groovyCustomizer"/>
<beans:bean id="groovyCustomizer" class="org.foo.MyGroovyObjectCustomizer"/>
----

View File

@@ -0,0 +1,119 @@
[[control-bus-controller]]
= Control Bus Controller
:page-section-summary-toc: 1
Starting with version 6.4, the HTTP module provides an `@EnableControlBusController` configuration class annotation to expose the `ControlBusController` as a REST service at the `/control-bus` path.
The `ControlBusControllerConfiguration` underneath enables eager initialization for the `ControlBusCommandRegistry` to expose all the available control bus commands for the mentioned REST service.
The `/control-bus` GET request returns all the control bus commands for the application in a format like this:
[source,json]
----
[
{
"beanName": "errorChannel",
"commands": [
{
"command": "errorChannel.setShouldTrack",
"description": "setShouldTrack",
"parameterTypes": [
"boolean"
]
},
{
"command": "errorChannel.setLoggingEnabled",
"description": "Use to disable debug logging during normal message flow",
"parameterTypes": [
"boolean"
]
},
{
"command": "errorChannel.isLoggingEnabled",
"description": "isLoggingEnabled",
"parameterTypes": []
}
]
},
{
"beanName": "testManagementComponent",
"commands": [
{
"command": "testManagementComponent.operation2",
"description": "operation2",
"parameterTypes": []
},
{
"command": "testManagementComponent.operation",
"description": "operation",
"parameterTypes": []
},
{
"command": "testManagementComponent.operation",
"description": "operation",
"parameterTypes": [
"int",
"java.lang.String"
]
},
{
"command": "testManagementComponent.operation",
"description": "operation",
"parameterTypes": [
"int"
]
}
]
}
]
----
Essentially, a JSON-serialized list of `ControlBusController.ControlBusBean` instances.
Each entry is a bean with a list of control bus eligible methods (see `ControlBusMethodFilter` for more information) with their parameter types and description from the `@ManagedOperation` or `@ManagedAttribute` (falls back to method name otherwise).
The GET method of this REST controller for `/control-bus/{beanName}` returns commands for specific bean.
The POST method to `/control-bus/{beanName.methodName}` invokes the command.
The body of the request may contain a list of values and their types for command to execute.
For example, the `operation` command with `int` argument for the class:
[source,java]
----
@ManagedResource
class TestManagementComponent {
@ManagedOperation
public void operation() {
}
@ManagedOperation(description = "The overloaded operation with int argument")
public void operation(int input) {
}
@ManagedOperation(description = "The overloaded operation with two arguments")
public void operation(int input1, String input2) {
}
@ManagedOperation
public int operation2() {
return 123;
}
}
----
could be called like `/testManagementComponent.operation` using mention POST method with body:
[source,json]
----
[
{
"value": "1",
"parameterType": "int"
}
]
----
See xref:control-bus.adoc[Control Bus] for more information.

View File

@@ -97,7 +97,7 @@ In fact, it applies to every other router, including expression-based routers, s
Any router that is a subclass of the `AbstractMappingMessageRouter` (which includes most framework-defined routers) is a dynamic router, because the `channelMapping` is defined at the `AbstractMappingMessageRouter` level.
That map's setter method is exposed as a public method along with the 'setChannelMapping' and 'removeChannelMapping' methods.
These let you change, add, and remove router mappings at runtime, as long as you have a reference to the router itself.
It also means that you could expose these same configuration options through JMX (see xref:jmx.adoc[JMX Support]) or the Spring Integration control bus (see xref:groovy.adoc#groovy-control-bus[Control Bus]) functionality.
It also means that you could expose these same configuration options through JMX (see xref:jmx.adoc[JMX Support]) or the Spring Integration control bus (see xref:control-bus.adoc[Control Bus]) functionality.
IMPORTANT: Falling back to the channel key as the channel name is flexible and convenient.
However, if you don't trust the message creator, a malicious actor (who has knowledge of the system) could create a message that is routed to an unexpected channel.
@@ -109,7 +109,7 @@ You may therefore wish to disable this feature (set the `channelKeyFallback` pro
One way to manage the router mappings is through the https://www.enterpriseintegrationpatterns.com/ControlBus.html[control bus] pattern, which exposes a control channel to which you can send control messages to manage and monitor Spring Integration components, including routers.
NOTE: For more information about the control bus, see xref:groovy.adoc#groovy-control-bus[Control Bus].
NOTE: For more information about the control bus, see xref:control-bus.adoc[Control Bus].
Typically, you would send a control message asking to invoke a particular operation on a particular managed component (such as a router).
The following managed operations (methods) are specific to changing the router resolution process:
@@ -125,15 +125,19 @@ The following methods let you do so:
* `public Map<String, String>getChannelMappings()`: Returns the current mappings.
* `public void replaceChannelMappings(Properties channelMappings)`: Updates the mappings.
Note that the `channelMappings` parameter is a `Properties` object.
This arrangement lets a control bus command use the built-in `StringToPropertiesConverter`, as the following example shows:
Note that the `channelMappings` parameter is a `Properties` object, so this has to be added to the respective `IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS` header:
[source]
----
"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')"
Properties newMapping = new Properties();
newMapping.setProperty("foo", "bar");
newMapping.setProperty("baz", "qux");
Message<?> replaceChannelMappingsCommandMessage =
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
.build();
----
Note that each mapping is separated by a newline character (`\n`).
For programmatic changes to the map, we recommend that you use the `setChannelMappings` method, due to type-safety concerns.
`replaceChannelMappings` ignores keys or values that are not `String` objects.

View File

@@ -279,7 +279,7 @@ If this attribute is not defined, the channel is always among the list of recipi
Starting with version 4.1, the `RecipientListRouter` provides several operations to manipulate recipients dynamically at runtime.
These management operations are presented by `RecipientListRouterManagement` through the `@ManagedResource` annotation.
They are available by using xref:groovy.adoc#groovy-control-bus[Control Bus] as well as by using JMX, as the following example shows:
They are available by using xref:control-bus.adoc[Control Bus] as well as by using JMX, as the following example shows:
[source,xml]
----
@@ -293,7 +293,10 @@ They are available by using xref:groovy.adoc#groovy-control-bus[Control Bus] as
----
[source,java]
----
messagingTemplate.convertAndSend(controlBus, "@'simpleRouter.handler'.addRecipient('channel2')");
Message<?> addRecipientCommandMessage =
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel2"))
.build();
----
From the application start up the `simpleRouter`, has only one `channel1` recipient.

View File

@@ -315,7 +315,7 @@ When all files are consumed, the remote fetch is attempted again, to pick up any
IMPORTANT: When you deploy multiple instances of an application, we recommend a small `max-fetch-size`, to avoid one instance "`grabbing`" all the files and starving other instances.
Another use for `max-fetch-size` is if you want to stop fetching remote files but continue to process files that have already been fetched.
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:groovy.adoc#groovy-control-bus[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:control-bus.adoc[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
If the poller is active when the property is changed, the change takes effect on the next poll.
The synchronizer can be provided with a `Comparator<SmbFile>`.

View File

@@ -16,6 +16,13 @@ In general the project has been moved to the latest dependency versions.
[[x6.4-new-components]]
=== New Components
The new Control Bus interaction model is implemented in the `ControlBusCommandRegistry`.
A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`.
See xref:control-bus.adoc[Control Bus] for more information.
Also, a `ControlBusController` (together with an `@EnableControlBusController`) is introduced for managing exposed commands by the mentioned `ControlBusCommandRegistry`.
See xref:http.adoc[HTTP Support] for more information.
[[x6.4-general]]
=== General Changes
@@ -34,13 +41,22 @@ The byte array handling for serialized message is fully deferred to JDBC driver.
The `LockRepository.delete()` method return the result of removing ownership of a distributed lock.
And the `JdbcLockRegistry.JdbcLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
See xref:jdbc.adoc[JDBC Support] for more information.
[[x6.4-zeromq-changes]]
=== ZeroMQ Changes
The outbound component `ZeroMqMessageHandler` (and respective API) can now bind a TCP port instead of connecting to a given URL.
See xref:zeromq.adoc[ZeroMQ Support] for more information.
[[x6.4-redis-changes]]
=== Redis Changes
Instead of throwing `IllegalStateException`, the `RedisLockRegistry.RedisLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
Instead of throwing `IllegalStateException`, the `RedisLockRegistry.RedisLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
See xref:redis.adoc[Redis Support] for more information.
[[x6.4-groovy-changes]]
=== Groovy Changes
The `ControlBusFactoryBean` (and respective `<int-groovy:control-bus>` XML tag) has been deprecated (for removal) in favor of new introduced `ControlBusFactoryBean` based on a new model implemented in the `ControlBusCommandRegistry`.
See xref:control-bus.adoc[Control Bus] for more information.