diff --git a/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java b/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java index f7226d0f1d..13ab0ad8dc 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/IntegrationMessageHeaderAccessor.java @@ -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 TYPE_VERIFY_MESSAGE_FUNCTION = (name, trailer) -> "The '" + name + trailer; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/ControlBusFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/ControlBusFactoryBean.java new file mode 100644 index 0000000000..293f897125 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/ControlBusFactoryBean.java @@ -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 { + + 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; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java index abb1957e85..136034b038 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java @@ -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) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/EnableIntegrationManagement.java b/spring-integration-core/src/main/java/org/springframework/integration/config/EnableIntegrationManagement.java index 6868f22ac0..496d56dc98 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/EnableIntegrationManagement.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/EnableIntegrationManagement.java @@ -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"; + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/ExpressionControlBusFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/ExpressionControlBusFactoryBean.java index 4bd88cc77d..f742c94343 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/ExpressionControlBusFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/ExpressionControlBusFactoryBean.java @@ -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 { - 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); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java index 9e430dae7c..3e58f9ab1e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java @@ -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) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ControlBusParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ControlBusParser.java index c2388f7a2d..7cedc96b53 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ControlBusParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ControlBusParser.java @@ -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 '' 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; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java index 51af94d311..f7d2f8a857 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationContextUtils.java @@ -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 diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java index 95e92c368f..cf377de2b4 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java @@ -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> 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> 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); } /** diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ControlBusMethodFilter.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ControlBusMethodFilter.java index 47757d41c6..1a8f8dd41a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/expression/ControlBusMethodFilter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ControlBusMethodFilter.java @@ -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 filter(List methods) { List 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); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/filter/AbstractMessageProcessingSelector.java b/spring-integration-core/src/main/java/org/springframework/integration/filter/AbstractMessageProcessingSelector.java index e9d9302431..b8efd698a9 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/filter/AbstractMessageProcessingSelector.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/filter/AbstractMessageProcessingSelector.java @@ -58,6 +58,10 @@ public abstract class AbstractMessageProcessingSelector } } + protected MessageProcessor getMessageProcessor() { + return this.messageProcessor; + } + @Override public final boolean accept(Message message) { Object result = this.messageProcessor.processMessage(message); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/filter/ExpressionEvaluatingSelector.java b/spring-integration-core/src/main/java/org/springframework/integration/filter/ExpressionEvaluatingSelector.java index afca2436f2..fec14a433f 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/filter/ExpressionEvaluatingSelector.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/filter/ExpressionEvaluatingSelector.java @@ -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 { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/filter/SimpleExpressionEvaluatingSelector.java b/spring-integration-core/src/main/java/org/springframework/integration/filter/SimpleExpressionEvaluatingSelector.java new file mode 100644 index 0000000000..22e9155c36 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/filter/SimpleExpressionEvaluatingSelector.java @@ -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 + "]"; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java index 3d6d3a6e45..3ae3577bb5 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java @@ -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(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/ControlBusMessageProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/ControlBusMessageProcessor.java new file mode 100644 index 0000000000..9f3463ac6e --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/ControlBusMessageProcessor.java @@ -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. + *

+ * 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 + 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 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); + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionCommandMessageProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionCommandMessageProcessor.java index 7a9b71f0b3..7c0b238f9e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionCommandMessageProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/ExpressionCommandMessageProcessor.java @@ -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 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'"); + } } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/support/MessagingMethodInvokerHelper.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/support/MessagingMethodInvokerHelper.java index eb92dd547a..74e37e7409 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/support/MessagingMethodInvokerHelper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/support/MessagingMethodInvokerHelper.java @@ -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) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java index 1566655fa2..38c969fa54 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouter.java @@ -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 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++; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouterManagement.java b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouterManagement.java index 86ec6c966c..fe24a8452d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouterManagement.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/RecipientListRouterManagement.java @@ -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 diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusCommandRegistry.java b/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusCommandRegistry.java new file mode 100644 index 0000000000..8a069b65fd --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusCommandRegistry.java @@ -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("^@?'?(?.[^']+)'?\\.(?[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> 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> getCommands() { + Map> commands = new HashMap<>(this.controlBusCommands.size()); + for (Map.Entry> beanEntry : this.controlBusCommands.entrySet()) { + Map 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 mappingFunction) { + + String beanName = commandMethod.beanName; + + Map 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 candidates = MethodIntrospector.selectMethods(bean.getClass(), + (ReflectionUtils.MethodFilter) method -> commandMethod.methodName.equals(method.getName())); + + Optional 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); + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusMethodFilter.java b/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusMethodFilter.java new file mode 100644 index 0000000000..7c186999d1 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/management/ControlBusMethodFilter.java @@ -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: + *
    + *
  • {@link Pausable} or {@link Lifecycle} components + *
  • {@code get}, {@code set} and {@code shutdown} methods of {@link CustomizableThreadCreator} + *
  • methods with {@link ManagedAttribute} or {@link ManagedOperation} annotations + *
+ * + * @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; + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/AbstractExpressionEvaluator.java b/spring-integration-core/src/main/java/org/springframework/integration/util/AbstractExpressionEvaluator.java index b75427755f..393ca9a7bb 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/AbstractExpressionEvaluator.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/AbstractExpressionEvaluator.java @@ -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 evaluateExpression(Expression expression, Message message, @Nullable Class expectedType) { try { diff --git a/spring-integration-core/src/main/kotlin/org/springframework/integration/dsl/KotlinIntegrationFlowDefinition.kt b/spring-integration-core/src/main/kotlin/org/springframework/integration/dsl/KotlinIntegrationFlowDefinition.kt index c6d96a1d00..5a049a944d 100644 --- a/spring-integration-core/src/main/kotlin/org/springframework/integration/dsl/KotlinIntegrationFlowDefinition.kt +++ b/spring-integration-core/src/main/kotlin/org/springframework/integration/dsl/KotlinIntegrationFlowDefinition.kt @@ -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.() -> 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.() -> Unit = {}) { this.delegate.controlBus(endpointConfigurer) } diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd index 15a9ada77c..8e0b448403 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd @@ -4968,6 +4968,16 @@ The list of component name patterns you want to track (e.g., tracked-components ]]> + + + + 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. + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests-context.xml index d1083dcd6f..a1bece4486 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests-context.xml @@ -20,6 +20,6 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests.java index 26e14b4fe7..52e6c732ce 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorWithMessageStoreParserTests.java @@ -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("@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 Message createMessage(T payload, Object correlationId, int sequenceSize, int sequenceNumber, MessageChannel outputChannel) { + return MessageBuilder.withPayload(payload) .setCorrelationId(correlationId) .setSequenceSize(sequenceSize) diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests-context.xml index c662a3c1a1..2f351a177e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests-context.xml @@ -14,7 +14,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests.java index 08c2c19b6c..447ad003ce 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusExplicitPollerTests.java @@ -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; } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusLifecycleTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusLifecycleTests-context.xml index 41bdf7368f..7db82494ba 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusLifecycleTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusLifecycleTests-context.xml @@ -7,7 +7,7 @@ https://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests-context.xml index f410c5ed7c..2baf9b2a0e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests-context.xml @@ -14,7 +14,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests.java index 668d0460d0..c486489f50 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusPollerTests.java @@ -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; } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests-context.xml index 6d25f57e47..dc24af6c4f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests-context.xml @@ -10,7 +10,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests.java index ebcf3291db..97d916dc25 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusRecipientListRouterTests.java @@ -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(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(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(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(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 map = new HashMap(); + Map 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(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(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); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests-context.xml index 66987f9b8c..dbb4d2c70c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests-context.xml @@ -15,7 +15,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java index 04b0c2602c..5ef146a733 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ControlBusTests.java @@ -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 diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index 93c0fbda84..12a971b7a2 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -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 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 diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java index 0e57f95fe7..0450468a65 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java @@ -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(); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollerAdviceTests.java index 478ad7accd..6efd745d75 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollerAdviceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollerAdviceTests.java @@ -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(); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/management/IntegrationManagementConfigurerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/management/IntegrationManagementConfigurerTests.java index eb202675ee..3541b41bae 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/management/IntegrationManagementConfigurerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/management/IntegrationManagementConfigurerTests.java @@ -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() { + 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> commands = + controlBusCommandRegistry.getCommands(); + + assertThat(commands).containsKeys("errorChannel", "nullChannel", "taskScheduler", "channel", + "_org.springframework.integration.errorLogger", + "_org.springframework.integration.errorLogger.handler"); + + Map commandMethodStringMap = commands.get("channel"); + + List 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); + } + + } + } diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/dsl/FileTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/dsl/FileTests.java index 5318b53a8c..3ae75ba605 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/dsl/FileTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/dsl/FileTests.java @@ -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 diff --git a/spring-integration-groovy/src/main/groovy/org/springframework/integration/groovy/dsl/GroovyIntegrationFlowDefinition.groovy b/spring-integration-groovy/src/main/groovy/org/springframework/integration/groovy/dsl/GroovyIntegrationFlowDefinition.groovy index dc975a36c6..086d726e0e 100644 --- a/spring-integration-groovy/src/main/groovy/org/springframework/integration/groovy/dsl/GroovyIntegrationFlowDefinition.groovy +++ b/spring-integration-groovy/src/main/groovy/org/springframework/integration/groovy/dsl/GroovyIntegrationFlowDefinition.groovy @@ -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, 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, strategy = Closure.DELEGATE_FIRST) @ClosureParams(value = SimpleType.class, options = 'org.springframework.integration.dsl.GenericEndpointSpec') diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/GroovyCommandMessageProcessor.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/GroovyCommandMessageProcessor.java index 499693499d..125083ccf9 100644 --- a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/GroovyCommandMessageProcessor.java +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/GroovyCommandMessageProcessor.java @@ -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 implements IntegrationPattern { diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusFactoryBean.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusFactoryBean.java index 6b40405b3d..024ef65500 100644 --- a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusFactoryBean.java +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusFactoryBean.java @@ -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 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 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 variables = new HashMap<>(); + variables.put("headers", message.getHeaders()); + return variables; + }); if (this.customizer != null) { processor.setCustomizer(this.customizer); } diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusParser.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusParser.java index f05780be34..dc7afa0b6d 100644 --- a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusParser.java +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyControlBusParser.java @@ -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"); diff --git a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyNamespaceHandler.java b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyNamespaceHandler.java index 9d9fbc29bd..d956b210b1 100644 --- a/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyNamespaceHandler.java +++ b/spring-integration-groovy/src/main/java/org/springframework/integration/groovy/config/GroovyNamespaceHandler.java @@ -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()); } } diff --git a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy.xsd b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy.xsd index 64a4f080fe..481011e405 100644 --- a/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy.xsd +++ b/spring-integration-groovy/src/main/resources/org/springframework/integration/groovy/config/spring-integration-groovy.xsd @@ -69,6 +69,7 @@ + [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, diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusDelayerManagementTest.groovy b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusDelayerManagementTest.groovy deleted file mode 100644 index 95d44e5978..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusDelayerManagementTest.groovy +++ /dev/null @@ -1,10 +0,0 @@ -def delayHandler = this.'testDelayer.handler' -def delayedMessageCount = delayHandler.delayedMessageCount - -println delayedMessageCount - -assert 2 == delayedMessageCount - -delayHandler.reschedulePersistedMessages() - -return true diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml deleted file mode 100644 index e920798e7f..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests.java deleted file mode 100644 index e043131356..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests.java +++ /dev/null @@ -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 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(); - } - -} diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptPayloadMessageProcessorTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptPayloadMessageProcessorTests.java deleted file mode 100644 index 402cfbfd29..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyScriptPayloadMessageProcessorTests.java +++ /dev/null @@ -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 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"); - } - -} diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests-context.xml b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests-context.xml deleted file mode 100644 index 74d4b12ffa..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests-context.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests.java b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests.java deleted file mode 100644 index 5e9b177386..0000000000 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/config/GroovyControlBusTests.java +++ /dev/null @@ -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 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(); - } - - } - -} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/ControlBusControllerConfiguration.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/ControlBusControllerConfiguration.java new file mode 100644 index 0000000000..c6fa6fdc27 --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/ControlBusControllerConfiguration.java @@ -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. + *

+ * 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); + } + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/EnableControlBusController.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/EnableControlBusController.java new file mode 100644 index 0000000000..53ec770a2b --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/EnableControlBusController.java @@ -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 { + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/management/ControlBusController.java b/spring-integration-http/src/main/java/org/springframework/integration/http/management/ControlBusController.java new file mode 100644 index 0000000000..047337b8a8 --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/management/ControlBusController.java @@ -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 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 commandsForBean = + this.controlBusCommandRegistry.getCommands() + .get(beanName); + + return createControlBusBean(beanName, commandsForBean); + } + + private ControlBusBean createControlBusBean(String beanName, + Map commandsForBean) { + + List 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 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 commands) { + + } + + public record ControlBusCommand(String command, String description, List> parameterTypes) { + + } + + public record CommandArgument(String value, Class parameterType) { + + } + +} diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/management/ControlBusControllerTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/management/ControlBusControllerTests.java new file mode 100644 index 0000000000..fc8c85d4da --- /dev/null +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/management/ControlBusControllerTests.java @@ -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; + } + + } + +} diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/ClientModeControlBusTests-context.xml b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/ClientModeControlBusTests-context.xml index 7f97b0d783..7a6b454fe7 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/ClientModeControlBusTests-context.xml +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/ClientModeControlBusTests-context.xml @@ -45,7 +45,7 @@ - + diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/StoredProcJavaConfigTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/StoredProcJavaConfigTests.java index 8f2ad69f11..ee9676a6fe 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/StoredProcJavaConfigTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/StoredProcJavaConfigTests.java @@ -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 diff --git a/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java b/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java index 68eeea42cd..7ae1bb5f15 100644 --- a/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java +++ b/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java @@ -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 diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests-context.xml index 3b6ac9ba11..20322c00b6 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests-context.xml @@ -21,7 +21,7 @@ - + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests.java b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests.java index 28206a8207..b956346c7d 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests.java +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/UpdateMappingsTests.java @@ -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("@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 map = new HashMap(); + Map 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"); diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/ControlBusParserTests-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/ControlBusParserTests-context.xml index caa3e50579..af2687a462 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/ControlBusParserTests-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/ControlBusParserTests-context.xml @@ -16,7 +16,7 @@ - + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml index cbbfcef799..bb15bb1a47 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml @@ -14,7 +14,7 @@ - + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration_/mbeanexporterhelper/INT-2626-config.xml b/spring-integration-jmx/src/test/java/org/springframework/integration_/mbeanexporterhelper/INT-2626-config.xml index b1d5fdcf46..f76d8f210c 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration_/mbeanexporterhelper/INT-2626-config.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration_/mbeanexporterhelper/INT-2626-config.xml @@ -17,6 +17,6 @@ selector-expression="payload.contains('stop')" /> - + diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/support/parametersource/ExpressionEvaluatingParameterSourceUtils.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/support/parametersource/ExpressionEvaluatingParameterSourceUtils.java index 8f34f9ecac..d3a941085f 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/support/parametersource/ExpressionEvaluatingParameterSourceUtils.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/support/parametersource/ExpressionEvaluatingParameterSourceUtils.java @@ -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(); } diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index 8e6b36c2b6..61e2b1e327 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -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[] diff --git a/src/reference/antora/modules/ROOT/pages/changes-1.0-2.0.adoc b/src/reference/antora/modules/ROOT/pages/changes-1.0-2.0.adoc index fcf2f9378d..7b6146bda7 100644 --- a/src/reference/antora/modules/ROOT/pages/changes-1.0-2.0.adoc +++ b/src/reference/antora/modules/ROOT/pages/changes-1.0-2.0.adoc @@ -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 diff --git a/src/reference/antora/modules/ROOT/pages/changes-5.1-5.2.adoc b/src/reference/antora/modules/ROOT/pages/changes-5.1-5.2.adoc index dfd24c651f..bd1699a18c 100644 --- a/src/reference/antora/modules/ROOT/pages/changes-5.1-5.2.adoc +++ b/src/reference/antora/modules/ROOT/pages/changes-5.1-5.2.adoc @@ -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>` 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. diff --git a/src/reference/antora/modules/ROOT/pages/configuration/annotations.adoc b/src/reference/antora/modules/ROOT/pages/configuration/annotations.adoc index e8f9d6c86c..c20cd53bab 100644 --- a/src/reference/antora/modules/ROOT/pages/configuration/annotations.adoc +++ b/src/reference/antora/modules/ROOT/pages/configuration/annotations.adoc @@ -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] diff --git a/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc b/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc index b993b28fd5..e631b390f2 100644 --- a/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc +++ b/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc @@ -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. diff --git a/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc b/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc index 9c49bf2e62..131b5a9426 100644 --- a/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc +++ b/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc @@ -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: diff --git a/src/reference/antora/modules/ROOT/pages/control-bus.adoc b/src/reference/antora/modules/ROOT/pages/control-bus.adoc index 210f7fc618..288be8a21d 100644 --- a/src/reference/antora/modules/ROOT/pages/control-bus.adoc +++ b/src/reference/antora/modules/ROOT/pages/control-bus.adoc @@ -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. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/delayer.adoc b/src/reference/antora/modules/ROOT/pages/delayer.adoc index b1b082cb60..cbb1a9acf3 100644 --- a/src/reference/antora/modules/ROOT/pages/delayer.adoc +++ b/src/reference/antora/modules/ROOT/pages/delayer.adoc @@ -192,7 +192,7 @@ These operations can be invoked through a `Control Bus` command, as the followin [source,java] ---- Message delayerReschedulingMessage = - MessageBuilder.withPayload("@'delayer.handler'.reschedulePersistedMessages()").build(); + MessageBuilder.withPayload("'delayer.handler'.reschedulePersistedMessages").build(); controlBusChannel.send(delayerReschedulingMessage); ---- diff --git a/src/reference/antora/modules/ROOT/pages/endpoint.adoc b/src/reference/antora/modules/ROOT/pages/endpoint.adoc index b72a4f072a..c2663753c8 100644 --- a/src/reference/antora/modules/ROOT/pages/endpoint.adoc +++ b/src/reference/antora/modules/ROOT/pages/endpoint.adoc @@ -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. diff --git a/src/reference/antora/modules/ROOT/pages/ftp/max-fetch.adoc b/src/reference/antora/modules/ROOT/pages/ftp/max-fetch.adoc index 4a1b212cac..26fea4c5ba 100644 --- a/src/reference/antora/modules/ROOT/pages/ftp/max-fetch.adoc +++ b/src/reference/antora/modules/ROOT/pages/ftp/max-fetch.adoc @@ -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`. diff --git a/src/reference/antora/modules/ROOT/pages/groovy.adoc b/src/reference/antora/modules/ROOT/pages/groovy.adoc index 22fbaaadd2..65d5011f90 100644 --- a/src/reference/antora/modules/ROOT/pages/groovy.adoc +++ b/src/reference/antora/modules/ROOT/pages/groovy.adoc @@ -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] ----- - ----- - -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] ----- - - - ----- diff --git a/src/reference/antora/modules/ROOT/pages/http/control-bus-controller.adoc b/src/reference/antora/modules/ROOT/pages/http/control-bus-controller.adoc new file mode 100644 index 0000000000..810d101f74 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/http/control-bus-controller.adoc @@ -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. + diff --git a/src/reference/antora/modules/ROOT/pages/router/dynamic-routers.adoc b/src/reference/antora/modules/ROOT/pages/router/dynamic-routers.adoc index 3ab0661c3a..022286fe48 100644 --- a/src/reference/antora/modules/ROOT/pages/router/dynamic-routers.adoc +++ b/src/reference/antora/modules/ROOT/pages/router/dynamic-routers.adoc @@ -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 MapgetChannelMappings()`: 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. diff --git a/src/reference/antora/modules/ROOT/pages/router/implementations.adoc b/src/reference/antora/modules/ROOT/pages/router/implementations.adoc index c5b9dca780..f8092f7894 100644 --- a/src/reference/antora/modules/ROOT/pages/router/implementations.adoc +++ b/src/reference/antora/modules/ROOT/pages/router/implementations.adoc @@ -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. diff --git a/src/reference/antora/modules/ROOT/pages/smb.adoc b/src/reference/antora/modules/ROOT/pages/smb.adoc index c6c84a2466..6d17fbf470 100644 --- a/src/reference/antora/modules/ROOT/pages/smb.adoc +++ b/src/reference/antora/modules/ROOT/pages/smb.adoc @@ -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`. diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 02dd1c76b8..0df8de265d 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -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. \ No newline at end of file +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 `` 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. \ No newline at end of file