GH-9381: Introduce Control Bus commands management
Fixes: #9381 Currently, there is no way to know in one place what Control Bus commands are available and with what arguments * Add `ControlBusCommandRegistry` infrastructure bean to gather control bus commands from beans and expose them for invocation * Add `ControlBusController` to expose a `/control-bus` REST service against the mentioned `ControlBusCommandRegistry` * Add `@EnableIntegrationManagement(loadControlBusCommands)` to be able to load all the Control Bus commands from the application context instead of on demand by default * Deprecated existing SpEL(and Groovy)-based Control Bus functionality in favor of new, more manageable, logic
This commit is contained in:
@@ -80,10 +80,15 @@ public class IntegrationMessageHeaderAccessor extends MessageHeaderAccessor {
|
||||
public static final String SOURCE_DATA = "sourceData";
|
||||
|
||||
/**
|
||||
* Raw source message.
|
||||
* The header for {@link reactor.util.context.ContextView}.
|
||||
*/
|
||||
public static final String REACTOR_CONTEXT = "reactorContext";
|
||||
|
||||
/**
|
||||
* The header for Control Bus command arguments. Must be a list of values.
|
||||
*/
|
||||
public static final String CONTROL_BUS_ARGUMENTS = "controlBusArguments";
|
||||
|
||||
private static final BiFunction<String, String, String> TYPE_VERIFY_MESSAGE_FUNCTION =
|
||||
(name, trailer) -> "The '" + name + trailer;
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.config;
|
||||
|
||||
import org.springframework.integration.handler.ControlBusMessageProcessor;
|
||||
import org.springframework.integration.handler.ServiceActivatingHandler;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
|
||||
/**
|
||||
* FactoryBean for creating {@link MessageHandler} instances to handle a message with a Control Bus command.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*/
|
||||
public class ControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler> {
|
||||
|
||||
private Long sendTimeout;
|
||||
|
||||
public void setSendTimeout(Long sendTimeout) {
|
||||
this.sendTimeout = sendTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageHandler createHandler() {
|
||||
ServiceActivatingHandler handler = new ServiceActivatingHandler(new ControlBusMessageProcessor());
|
||||
if (this.sendTimeout != null) {
|
||||
handler.setSendTimeout(this.sendTimeout);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,8 +17,6 @@
|
||||
package org.springframework.integration.config;
|
||||
|
||||
import org.springframework.expression.MethodFilter;
|
||||
import org.springframework.integration.expression.ControlBusMethodFilter;
|
||||
import org.springframework.integration.handler.ExpressionCommandMessageProcessor;
|
||||
import org.springframework.integration.handler.ServiceActivatingHandler;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
|
||||
@@ -31,10 +29,14 @@ import org.springframework.messaging.MessageHandler;
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0
|
||||
*
|
||||
* @deprecated in favor of {@link ControlBusFactoryBean}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public class ExpressionControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler> {
|
||||
|
||||
private static final MethodFilter METHOD_FILTER = new ControlBusMethodFilter();
|
||||
@SuppressWarnings("removal")
|
||||
private static final MethodFilter METHOD_FILTER = new org.springframework.integration.expression.ControlBusMethodFilter();
|
||||
|
||||
private Long sendTimeout;
|
||||
|
||||
@@ -42,10 +44,11 @@ public class ExpressionControlBusFactoryBean extends AbstractSimpleMessageHandle
|
||||
this.sendTimeout = sendTimeout;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Override
|
||||
protected MessageHandler createHandler() {
|
||||
ExpressionCommandMessageProcessor processor =
|
||||
new ExpressionCommandMessageProcessor(METHOD_FILTER, getBeanFactory());
|
||||
org.springframework.integration.handler.ExpressionCommandMessageProcessor processor =
|
||||
new org.springframework.integration.handler.ExpressionCommandMessageProcessor(METHOD_FILTER, getBeanFactory());
|
||||
ServiceActivatingHandler handler = new ServiceActivatingHandler(processor);
|
||||
if (this.sendTimeout != null) {
|
||||
handler.setSendTimeout(this.sendTimeout);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,19 +20,30 @@ import org.w3c.dom.Element;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.integration.config.ControlBusFactoryBean;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ControlBusParser extends AbstractConsumerEndpointParser {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
protected BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
"org.springframework.integration.config.ExpressionControlBusFactoryBean");
|
||||
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "bean-resolver");
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ControlBusFactoryBean.class);
|
||||
if (Boolean.FALSE.equals(Boolean.parseBoolean(element.getAttribute("use-registry")))) {
|
||||
builder = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
org.springframework.integration.config.ExpressionControlBusFactoryBean.class);
|
||||
parserContext.getReaderContext()
|
||||
.warning("The 'ExpressionControlBusFactoryBean' for '<control-bus>' is deprecated (for removal) " +
|
||||
"in favor of 'ControlBusFactoryBean'. " +
|
||||
"Set 'use-registry' attribute to 'true' to switch to a new functionality.",
|
||||
element);
|
||||
}
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-timeout");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "order");
|
||||
return builder;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,7 +52,6 @@ import org.springframework.integration.core.MessageProducer;
|
||||
import org.springframework.integration.core.MessageSelector;
|
||||
import org.springframework.integration.dsl.support.FixedSubscriberChannelPrototype;
|
||||
import org.springframework.integration.dsl.support.MessageChannelReference;
|
||||
import org.springframework.integration.expression.ControlBusMethodFilter;
|
||||
import org.springframework.integration.expression.FunctionExpression;
|
||||
import org.springframework.integration.filter.ExpressionEvaluatingSelector;
|
||||
import org.springframework.integration.filter.MessageFilter;
|
||||
@@ -60,8 +59,8 @@ import org.springframework.integration.filter.MethodInvokingSelector;
|
||||
import org.springframework.integration.handler.AbstractMessageProducingHandler;
|
||||
import org.springframework.integration.handler.BeanNameMessageProcessor;
|
||||
import org.springframework.integration.handler.BridgeHandler;
|
||||
import org.springframework.integration.handler.ControlBusMessageProcessor;
|
||||
import org.springframework.integration.handler.DelayHandler;
|
||||
import org.springframework.integration.handler.ExpressionCommandMessageProcessor;
|
||||
import org.springframework.integration.handler.LambdaMessageProcessor;
|
||||
import org.springframework.integration.handler.LoggingHandler;
|
||||
import org.springframework.integration.handler.MessageProcessor;
|
||||
@@ -517,8 +516,33 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
|
||||
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @return the current {@link BaseIntegrationFlowDefinition}.
|
||||
* @see ExpressionCommandMessageProcessor
|
||||
* @since 6.4
|
||||
* @see ControlBusMessageProcessor
|
||||
*/
|
||||
public B controlBusOnRegistry() {
|
||||
return controlBusOnRegistry(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
|
||||
* @return the current {@link BaseIntegrationFlowDefinition}.
|
||||
* @since 6.4
|
||||
* @see GenericEndpointSpec
|
||||
* @see ControlBusMessageProcessor
|
||||
*/
|
||||
public B controlBusOnRegistry(@Nullable Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
|
||||
return handle(new ServiceActivatingHandler(new ControlBusMessageProcessor()), endpointConfigurer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @return the current {@link BaseIntegrationFlowDefinition}.
|
||||
* @deprecated in favor of {@link #controlBusOnRegistry()}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public B controlBus() {
|
||||
return controlBus(null);
|
||||
}
|
||||
@@ -528,12 +552,15 @@ public abstract class BaseIntegrationFlowDefinition<B extends BaseIntegrationFlo
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
|
||||
* @return the current {@link BaseIntegrationFlowDefinition}.
|
||||
* @see ExpressionCommandMessageProcessor
|
||||
* @deprecated in favor of {@link #controlBusOnRegistry(Consumer)}
|
||||
* @see GenericEndpointSpec
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public B controlBus(@Nullable Consumer<GenericEndpointSpec<ServiceActivatingHandler>> endpointConfigurer) {
|
||||
return handle(new ServiceActivatingHandler(new ExpressionCommandMessageProcessor(
|
||||
new ControlBusMethodFilter())), endpointConfigurer);
|
||||
return handle(new ServiceActivatingHandler(
|
||||
new org.springframework.integration.handler.ExpressionCommandMessageProcessor(
|
||||
new org.springframework.integration.expression.ControlBusMethodFilter())), endpointConfigurer);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
* Copyright 2014-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,9 +21,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.annotation.AnnotationFilter;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.RepeatableContainers;
|
||||
import org.springframework.expression.MethodFilter;
|
||||
import org.springframework.integration.core.Pausable;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
@@ -45,9 +42,15 @@ import org.springframework.util.ReflectionUtils;
|
||||
* @author Gary Russell
|
||||
*
|
||||
* @since 4.0
|
||||
*
|
||||
* @deprecated in favor of {@link org.springframework.integration.support.management.ControlBusMethodFilter}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public class ControlBusMethodFilter implements MethodFilter {
|
||||
|
||||
private static final ReflectionUtils.MethodFilter CONTROL_BUS_METHOD_FILTER =
|
||||
new org.springframework.integration.support.management.ControlBusMethodFilter();
|
||||
|
||||
@Override
|
||||
public List<Method> filter(List<Method> methods) {
|
||||
List<Method> supportedMethods = new ArrayList<>();
|
||||
@@ -60,26 +63,7 @@ public class ControlBusMethodFilter implements MethodFilter {
|
||||
}
|
||||
|
||||
private boolean accept(Method method) {
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
String methodName = method.getName();
|
||||
if ((Pausable.class.isAssignableFrom(declaringClass) || Lifecycle.class.isAssignableFrom(declaringClass))
|
||||
&& ReflectionUtils.findMethod(Pausable.class, methodName, method.getParameterTypes()) != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CustomizableThreadCreator.class.isAssignableFrom(declaringClass)
|
||||
&& (methodName.startsWith("get")
|
||||
|| methodName.startsWith("set")
|
||||
|| methodName.startsWith("shutdown"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MergedAnnotations mergedAnnotations =
|
||||
MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
|
||||
RepeatableContainers.none(), AnnotationFilter.PLAIN);
|
||||
|
||||
return mergedAnnotations.get(ManagedAttribute.class).isPresent()
|
||||
|| mergedAnnotations.get(ManagedOperation.class).isPresent();
|
||||
return CONTROL_BUS_METHOD_FILTER.matches(method);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ public abstract class AbstractMessageProcessingSelector
|
||||
}
|
||||
}
|
||||
|
||||
protected MessageProcessor<Boolean> getMessageProcessor() {
|
||||
return this.messageProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean accept(Message<?> message) {
|
||||
Object result = this.messageProcessor.processMessage(message);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.handler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.IntegrationPattern;
|
||||
import org.springframework.integration.IntegrationPatternType;
|
||||
import org.springframework.integration.support.management.ControlBusCommandRegistry;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A MessageProcessor implementation that expects a Control Bus command as a request message.
|
||||
* When processing, it evaluates a SpEL expression associated with requested command,
|
||||
* essentially target bean method invocation.
|
||||
* <p>
|
||||
* The arguments for the command must be provided
|
||||
* in the {@link IntegrationMessageHeaderAccessor#CONTROL_BUS_ARGUMENTS} message header.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*/
|
||||
public class ControlBusMessageProcessor extends AbstractMessageProcessor<Object>
|
||||
implements IntegrationPattern {
|
||||
|
||||
private ControlBusCommandRegistry controlBusCommandRegistry;
|
||||
|
||||
public ControlBusMessageProcessor() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance based on the provided {@link ControlBusCommandRegistry}.
|
||||
* @param controlBusCommandRegistry the {@link ControlBusCommandRegistry} with commands to execute.
|
||||
*/
|
||||
public ControlBusMessageProcessor(ControlBusCommandRegistry controlBusCommandRegistry) {
|
||||
this.controlBusCommandRegistry = controlBusCommandRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegrationPatternType getIntegrationPatternType() {
|
||||
return IntegrationPatternType.control_bus;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInit() {
|
||||
super.onInit();
|
||||
if (this.controlBusCommandRegistry == null) {
|
||||
this.controlBusCommandRegistry = getBeanFactory().getBean(ControlBusCommandRegistry.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object processMessage(Message<?> message) {
|
||||
String command = message.getPayload().toString();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> arguments =
|
||||
message.getHeaders().get(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.class);
|
||||
Class<?>[] parameterTypes = new Class<?>[0];
|
||||
if (!CollectionUtils.isEmpty(arguments)) {
|
||||
parameterTypes =
|
||||
arguments.stream()
|
||||
.map(Object::getClass)
|
||||
.toArray(Class<?>[]::new);
|
||||
}
|
||||
Expression commandExpression = this.controlBusCommandRegistry.getExpressionForCommand(command, parameterTypes);
|
||||
return evaluateExpression(commandExpression, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.springframework.expression.MethodExecutor;
|
||||
import org.springframework.expression.MethodFilter;
|
||||
import org.springframework.expression.MethodResolver;
|
||||
import org.springframework.expression.spel.support.ReflectiveMethodResolver;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.integration.IntegrationPattern;
|
||||
import org.springframework.integration.IntegrationPatternType;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -47,7 +48,10 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.0
|
||||
*
|
||||
* @deprecated in favor of {@link ControlBusMessageProcessor}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public class ExpressionCommandMessageProcessor extends AbstractMessageProcessor<Object>
|
||||
implements IntegrationPattern {
|
||||
|
||||
@@ -74,7 +78,12 @@ public class ExpressionCommandMessageProcessor extends AbstractMessageProcessor<
|
||||
super.setBeanFactory(beanFactory);
|
||||
if (this.methodFilter != null) {
|
||||
MethodResolver methodResolver = new ExpressionCommandMethodResolver(this.methodFilter);
|
||||
getEvaluationContext().setMethodResolvers(Collections.singletonList(methodResolver));
|
||||
if (getEvaluationContext() instanceof StandardEvaluationContext standardEvaluationContext) {
|
||||
standardEvaluationContext.setMethodResolvers(Collections.singletonList(methodResolver));
|
||||
}
|
||||
else {
|
||||
logger.warn("Cannot customize the 'SimpleEvaluationContext'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.integration.IntegrationPatternType;
|
||||
import org.springframework.integration.core.MessageSelector;
|
||||
import org.springframework.integration.filter.ExpressionEvaluatingSelector;
|
||||
import org.springframework.integration.filter.SimpleExpressionEvaluatingSelector;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -135,8 +136,8 @@ public class RecipientListRouter extends AbstractMessageRouter implements Recipi
|
||||
private void addRecipient(String channelName, String selectorExpression, Queue<Recipient> recipientsToAdd) {
|
||||
Assert.hasText(channelName, "'channelName' must not be empty.");
|
||||
Assert.hasText(selectorExpression, "'selectorExpression' must not be empty.");
|
||||
ExpressionEvaluatingSelector expressionEvaluatingSelector =
|
||||
new ExpressionEvaluatingSelector(selectorExpression);
|
||||
SimpleExpressionEvaluatingSelector expressionEvaluatingSelector =
|
||||
new SimpleExpressionEvaluatingSelector(selectorExpression);
|
||||
expressionEvaluatingSelector.setBeanFactory(getBeanFactory());
|
||||
Recipient recipient = new Recipient(channelName, expressionEvaluatingSelector);
|
||||
setupRecipient(recipient);
|
||||
@@ -203,9 +204,13 @@ public class RecipientListRouter extends AbstractMessageRouter implements Recipi
|
||||
Recipient next = it.next();
|
||||
MessageSelector selector = next.getSelector();
|
||||
MessageChannel channel = next.getChannel();
|
||||
if (selector instanceof ExpressionEvaluatingSelector
|
||||
if ((selector instanceof ExpressionEvaluatingSelector expressionEvaluatingSelector
|
||||
&& targetChannel.equals(channel)
|
||||
&& ((ExpressionEvaluatingSelector) selector).getExpressionString().equals(selectorExpression)) {
|
||||
&& expressionEvaluatingSelector.getExpressionString().equals(selectorExpression)) ||
|
||||
(selector instanceof SimpleExpressionEvaluatingSelector simpleExpressionEvaluatingSelector
|
||||
&& targetChannel.equals(channel)
|
||||
&& simpleExpressionEvaluatingSelector.getExpressionString().equals(selectorExpression))) {
|
||||
|
||||
it.remove();
|
||||
counter++;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.support.management;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CustomizableThreadCreator;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A global component to serve Control Bus command and respective SpEL expression relationships.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*
|
||||
* @see ControlBusMethodFilter
|
||||
*/
|
||||
public class ControlBusCommandRegistry
|
||||
implements ApplicationContextAware, SmartInitializingSingleton, DestructionAwareBeanPostProcessor {
|
||||
|
||||
private static final Pattern COMMAND_PATTERN =
|
||||
Pattern.compile("^@?'?(?<beanName>.[^']+)'?\\.(?<methodName>[a-zA-Z0-9_]+)\\(?\\)?$");
|
||||
|
||||
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
|
||||
|
||||
private static final ControlBusMethodFilter CONTROL_BUS_METHOD_FILTER = new ControlBusMethodFilter();
|
||||
|
||||
private final Map<String, Map<CommandMethod, Expression>> controlBusCommands = new HashMap<>();
|
||||
|
||||
private boolean eagerInitialization;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private boolean initialized;
|
||||
|
||||
/**
|
||||
* Set to {@code true} to turn on Control Bus commands loading after application context initialization.
|
||||
* @param eagerInitialization true to initialize this registry eagerly.
|
||||
*/
|
||||
public void setEagerInitialization(boolean eagerInitialization) {
|
||||
this.eagerInitialization = eagerInitialization;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
if (this.eagerInitialization) {
|
||||
this.applicationContext.getBeansOfType(null)
|
||||
.forEach(this::registerControlBusCommands);
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
// Only for dynamically created beans. Others are registered by afterSingletonsInstantiated()
|
||||
if (this.initialized) {
|
||||
registerControlBusCommands(beanName, bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually register Control Bus commands (if any) for a specific bean.
|
||||
* The bean must be in instance of {@link Lifecycle}, or {@link CustomizableThreadCreator},
|
||||
* or marked with the {@link ManagedResource}, or {@link IntegrationManagedResource} annotation.
|
||||
* @param beanName the bean name for registration
|
||||
* @param bean the bean for registration
|
||||
*/
|
||||
public void registerControlBusCommands(String beanName, Object bean) {
|
||||
Class<?> beanClass = bean.getClass();
|
||||
if (bean instanceof Lifecycle || bean instanceof CustomizableThreadCreator
|
||||
|| AnnotationUtils.findAnnotation(beanClass, ManagedResource.class) != null
|
||||
|| AnnotationUtils.findAnnotation(beanClass, IntegrationManagedResource.class) != null) {
|
||||
|
||||
ReflectionUtils.doWithMethods(beanClass, method -> populateExpressionForCommand(beanName, method),
|
||||
CONTROL_BUS_METHOD_FILTER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
|
||||
this.controlBusCommands.remove(beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return registered Control Bus commands.
|
||||
* @return registered commands.
|
||||
*/
|
||||
public Map<String, Map<CommandMethod, String>> getCommands() {
|
||||
Map<String, Map<CommandMethod, String>> commands = new HashMap<>(this.controlBusCommands.size());
|
||||
for (Map.Entry<String, Map<CommandMethod, Expression>> beanEntry : this.controlBusCommands.entrySet()) {
|
||||
Map<CommandMethod, String> commandEntries =
|
||||
beanEntry.getValue()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||
(commandEntry) -> commandEntry.getValue().getExpressionString()));
|
||||
commands.put(beanEntry.getKey(), commandEntries);
|
||||
}
|
||||
return Collections.unmodifiableMap(commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a SpEL expression for the command to call with parameter types.
|
||||
* The command must be in format {@code beanName.methodName}.
|
||||
* (Or {@code @beanName.methodName()} for backward compatibility with simple expressions.)
|
||||
* If {@code beanName} is a complex literal, it has to be wrapped into single quotes,
|
||||
* e.g. {@code 'some.complex.bean-name'}.
|
||||
* The target method to call must fit into the {@link ControlBusMethodFilter} requirements
|
||||
* and match with the provided parameter types.
|
||||
* @param command the command to call.
|
||||
* @param parameterTypes the parameter types for the target method to call.
|
||||
* @return the SpEL expression for the provided command.
|
||||
* @throws IllegalArgumentException if provided command does not match to Control Bus requirements
|
||||
* or target method contract.
|
||||
*/
|
||||
public Expression getExpressionForCommand(String command, Class<?>... parameterTypes) {
|
||||
Matcher matcher = COMMAND_PATTERN.matcher(command);
|
||||
Assert.isTrue(matcher.matches(),
|
||||
"The command must be in format 'beanName.methodName'. " +
|
||||
"Arguments must go to the 'controlBusArguments' message header.");
|
||||
|
||||
String beanName = matcher.group("beanName");
|
||||
Assert.isTrue(this.applicationContext.containsBean(beanName),
|
||||
() -> "There is no registered bean for requested command: " + beanName);
|
||||
|
||||
String methodName = matcher.group("methodName");
|
||||
|
||||
CommandMethod commandMethod = new CommandMethod(beanName, methodName, parameterTypes);
|
||||
return populateCommandMethod(commandMethod, (key) -> buildExpressionForMethodToCall(commandMethod));
|
||||
}
|
||||
|
||||
private void populateExpressionForCommand(String beanName, Method methodForCommand) {
|
||||
CommandMethod commandMethod =
|
||||
new CommandMethod(beanName, methodForCommand.getName(), methodForCommand.getParameterTypes());
|
||||
populateCommandMethod(commandMethod, (key) -> buildExpressionForMethodToCall(commandMethod, methodForCommand));
|
||||
}
|
||||
|
||||
private Expression populateCommandMethod(CommandMethod commandMethod,
|
||||
Function<CommandMethod, Expression> mappingFunction) {
|
||||
|
||||
String beanName = commandMethod.beanName;
|
||||
|
||||
Map<CommandMethod, Expression> beanControlBusCommands =
|
||||
this.controlBusCommands.computeIfAbsent(beanName, (key) -> new HashMap<>());
|
||||
|
||||
try {
|
||||
return beanControlBusCommands.computeIfAbsent(commandMethod, mappingFunction);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (beanControlBusCommands.isEmpty()) {
|
||||
this.controlBusCommands.remove(beanName);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private Expression buildExpressionForMethodToCall(CommandMethod commandMethod) {
|
||||
Object bean = this.applicationContext.getBean(commandMethod.beanName);
|
||||
Set<Method> candidates = MethodIntrospector.selectMethods(bean.getClass(),
|
||||
(ReflectionUtils.MethodFilter) method -> commandMethod.methodName.equals(method.getName()));
|
||||
|
||||
Optional<Method> methodForCommand =
|
||||
candidates.stream()
|
||||
.filter(method ->
|
||||
areParameterTypesEqual(method.getParameterTypes(), commandMethod.parameterTypes))
|
||||
.findFirst();
|
||||
|
||||
Assert.isTrue(methodForCommand.isPresent(),
|
||||
() -> "No method '%s' found in bean '%s' for parameter types '%s'"
|
||||
.formatted(commandMethod.methodName, bean, Arrays.toString(commandMethod.parameterTypes)));
|
||||
|
||||
return buildExpressionForMethodToCall(commandMethod, methodForCommand.get());
|
||||
}
|
||||
|
||||
private static Expression buildExpressionForMethodToCall(CommandMethod commandMethod, Method methodForCommand) {
|
||||
Assert.isTrue(CONTROL_BUS_METHOD_FILTER.matches(methodForCommand),
|
||||
() -> "The method '%s' is not valid Control Bus command".formatted(methodForCommand));
|
||||
|
||||
populateDescriptionIntoCommand(commandMethod, methodForCommand);
|
||||
|
||||
Class<?>[] parameterTypes = methodForCommand.getParameterTypes();
|
||||
StringBuilder expressionBuilder =
|
||||
new StringBuilder("@'")
|
||||
.append(commandMethod.beanName)
|
||||
.append("'.")
|
||||
.append(methodForCommand.getName())
|
||||
.append('(');
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
expressionBuilder.append('[').append(i).append("],");
|
||||
}
|
||||
if (parameterTypes.length > 0) {
|
||||
expressionBuilder.deleteCharAt(expressionBuilder.length() - 1);
|
||||
}
|
||||
String expression = expressionBuilder.append(')').toString();
|
||||
return EXPRESSION_PARSER.parseExpression(expression);
|
||||
}
|
||||
|
||||
private static boolean areParameterTypesEqual(Class<?>[] lhsTypes, Class<?>[] rhsTypes) {
|
||||
if (lhsTypes.length == rhsTypes.length) {
|
||||
for (int i = 0; i < lhsTypes.length; i++) {
|
||||
if (!ClassUtils.isAssignable(lhsTypes[i], rhsTypes[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void populateDescriptionIntoCommand(CommandMethod commandMethod, Method methodForCommand) {
|
||||
ManagedOperation managedOperation = AnnotationUtils.findAnnotation(methodForCommand, ManagedOperation.class);
|
||||
if (managedOperation != null) {
|
||||
commandMethod.description = managedOperation.description();
|
||||
}
|
||||
else {
|
||||
ManagedAttribute managedAttribute = AnnotationUtils.findAnnotation(methodForCommand, ManagedAttribute.class);
|
||||
if (managedAttribute != null) {
|
||||
commandMethod.description = managedAttribute.description();
|
||||
}
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(commandMethod.description)) {
|
||||
commandMethod.description = commandMethod.methodName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Java Bean to represent a Control Bus command as a bean method with its parameter types.
|
||||
*/
|
||||
public static final class CommandMethod {
|
||||
|
||||
private final String beanName;
|
||||
|
||||
private final String methodName;
|
||||
|
||||
private final Class<?>[] parameterTypes;
|
||||
|
||||
private String description;
|
||||
|
||||
private CommandMethod(String beanName, String methodName, Class<?>[] parameterTypes) {
|
||||
this.beanName = beanName;
|
||||
this.methodName = methodName;
|
||||
this.parameterTypes = parameterTypes;
|
||||
}
|
||||
|
||||
public String getBeanName() {
|
||||
return this.beanName;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return this.methodName;
|
||||
}
|
||||
|
||||
public Class<?>[] getParameterTypes() {
|
||||
return this.parameterTypes;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(this.beanName, this.methodName);
|
||||
result = 31 * result + Arrays.hashCode(this.parameterTypes);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
CommandMethod that = (CommandMethod) o;
|
||||
return Objects.equals(this.beanName, that.beanName)
|
||||
&& Objects.equals(this.methodName, that.methodName)
|
||||
&& areParameterTypesEqual(this.parameterTypes, that.parameterTypes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2014-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.support.management;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.annotation.AnnotationFilter;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.RepeatableContainers;
|
||||
import org.springframework.integration.core.Pausable;
|
||||
import org.springframework.jmx.export.annotation.ManagedAttribute;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.util.CustomizableThreadCreator;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* The {@link ReflectionUtils.MethodFilter} to restrict method invocations to:
|
||||
* <ul>
|
||||
* <li> {@link Pausable} or {@link Lifecycle} components
|
||||
* <li> {@code get}, {@code set} and {@code shutdown} methods of {@link CustomizableThreadCreator}
|
||||
* <li> methods with {@link ManagedAttribute} or {@link ManagedOperation} annotations
|
||||
* </ul>
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*/
|
||||
public class ControlBusMethodFilter implements ReflectionUtils.MethodFilter {
|
||||
|
||||
@Override
|
||||
public boolean matches(Method method) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
String methodName = method.getName();
|
||||
if ((Pausable.class.isAssignableFrom(declaringClass) || Lifecycle.class.isAssignableFrom(declaringClass))
|
||||
&& ReflectionUtils.findMethod(Pausable.class, methodName, method.getParameterTypes()) != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CustomizableThreadCreator.class.isAssignableFrom(declaringClass)
|
||||
&& (methodName.startsWith("get")
|
||||
|| methodName.startsWith("set")
|
||||
|| methodName.equals("shutdown"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
MergedAnnotations mergedAnnotations =
|
||||
MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY,
|
||||
RepeatableContainers.none(), AnnotationFilter.PLAIN);
|
||||
|
||||
return mergedAnnotations.get(ManagedAttribute.class).isPresent()
|
||||
|| mergedAnnotations.get(ManagedOperation.class).isPresent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,9 @@ package org.springframework.integration.util;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.expression.BeanFactoryResolver;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
@@ -51,7 +51,9 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
|
||||
|
||||
private final BeanFactoryTypeConverter typeConverter = new BeanFactoryTypeConverter();
|
||||
|
||||
private volatile StandardEvaluationContext evaluationContext;
|
||||
private boolean simpleEvaluationContext;
|
||||
|
||||
private volatile EvaluationContext evaluationContext;
|
||||
|
||||
private volatile BeanFactory beanFactory;
|
||||
|
||||
@@ -64,9 +66,6 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
|
||||
public void setBeanFactory(BeanFactory beanFactory) {
|
||||
this.beanFactory = beanFactory;
|
||||
this.typeConverter.setBeanFactory(beanFactory);
|
||||
if (this.evaluationContext != null && this.evaluationContext.getBeanResolver() == null) {
|
||||
this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
|
||||
}
|
||||
}
|
||||
|
||||
protected BeanFactory getBeanFactory() {
|
||||
@@ -83,6 +82,17 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
|
||||
return this.messageBuilderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flag to indicate that a {@link org.springframework.expression.spel.support.SimpleEvaluationContext}
|
||||
* must be used for expression evaluations.
|
||||
* @param simpleEvaluationContext true to use the
|
||||
* {@link org.springframework.expression.spel.support.SimpleEvaluationContext}
|
||||
* @since 6.4
|
||||
*/
|
||||
public void setSimpleEvaluationContext(boolean simpleEvaluationContext) {
|
||||
this.simpleEvaluationContext = simpleEvaluationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void afterPropertiesSet() {
|
||||
getEvaluationContext();
|
||||
@@ -93,24 +103,21 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
|
||||
onInit();
|
||||
}
|
||||
|
||||
protected StandardEvaluationContext getEvaluationContext() {
|
||||
protected EvaluationContext getEvaluationContext() {
|
||||
return getEvaluationContext(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a WARN log if the beanFactory field is null, unless the argument is false.
|
||||
* @param beanFactoryRequired set to false to suppress the warning.
|
||||
* @param beanFactoryRequired set to {@code false} to suppress the warning.
|
||||
* @return The evaluation context.
|
||||
*/
|
||||
protected final StandardEvaluationContext getEvaluationContext(boolean beanFactoryRequired) {
|
||||
protected final EvaluationContext getEvaluationContext(boolean beanFactoryRequired) {
|
||||
if (this.evaluationContext == null) {
|
||||
if (this.beanFactory == null && !beanFactoryRequired) {
|
||||
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext();
|
||||
this.evaluationContext = obtainEvaluationContext(beanFactoryRequired);
|
||||
if (this.evaluationContext instanceof StandardEvaluationContext standardEvaluationContext) {
|
||||
standardEvaluationContext.setTypeConverter(this.typeConverter);
|
||||
}
|
||||
else {
|
||||
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
|
||||
}
|
||||
this.evaluationContext.setTypeConverter(this.typeConverter);
|
||||
if (this.beanFactory != null) {
|
||||
ConversionService conversionService = IntegrationUtils.getConversionService(this.beanFactory);
|
||||
if (conversionService != null) {
|
||||
@@ -121,6 +128,20 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware, I
|
||||
return this.evaluationContext;
|
||||
}
|
||||
|
||||
private EvaluationContext obtainEvaluationContext(boolean beanFactoryRequired) {
|
||||
if (this.beanFactory == null && !beanFactoryRequired) {
|
||||
return
|
||||
this.simpleEvaluationContext
|
||||
? ExpressionUtils.createSimpleEvaluationContext()
|
||||
: ExpressionUtils.createStandardEvaluationContext();
|
||||
}
|
||||
else {
|
||||
return this.simpleEvaluationContext
|
||||
? ExpressionUtils.createSimpleEvaluationContext(this.beanFactory)
|
||||
: ExpressionUtils.createStandardEvaluationContext(this.beanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected <T> T evaluateExpression(Expression expression, Message<?> message, @Nullable Class<T> expectedType) {
|
||||
try {
|
||||
|
||||
@@ -283,7 +283,19 @@ class KotlinIntegrationFlowDefinition(@PublishedApi internal val delegate: Integ
|
||||
/**
|
||||
* Populate the `Control Bus` EI Pattern specific [MessageHandler] implementation
|
||||
* at the current [IntegrationFlow] chain position.
|
||||
* @since 6.4
|
||||
*/
|
||||
fun controlBusOnRegistry(endpointConfigurer: GenericEndpointSpec<ServiceActivatingHandler>.() -> Unit = {}) {
|
||||
this.delegate.controlBusOnRegistry(endpointConfigurer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the `Control Bus` EI Pattern specific [MessageHandler] implementation
|
||||
* at the current [IntegrationFlow] chain position.
|
||||
*/
|
||||
@Deprecated("Use 'controlBusOnRegistry()' instead.",
|
||||
replaceWith = ReplaceWith("controlBusOnRegistry()"))
|
||||
@Suppress("DEPRECATION", "REMOVAL")
|
||||
fun controlBus(endpointConfigurer: GenericEndpointSpec<ServiceActivatingHandler>.() -> Unit = {}) {
|
||||
this.delegate.controlBus(endpointConfigurer)
|
||||
}
|
||||
|
||||
@@ -4968,6 +4968,16 @@ The list of component name patterns you want to track (e.g., tracked-components
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
<xsd:attribute name="id" type="xsd:string"/>
|
||||
<xsd:attribute name="use-registry" type="xsd:boolean" default="false">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
Set true to make Control Bus based on the global 'ControlBusCommandRegistry'
|
||||
which is a recommended way to configure Control Bus functionality.
|
||||
The false is by default for backward compatibility and is deprecated.
|
||||
This attributed will be true by default in the next major version and removed altogether eventually.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="retryAdviceType">
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
<beans:bean id="aggregatorBean"
|
||||
class="org.springframework.integration.config.TestAggregatorBean" />
|
||||
|
||||
<control-bus input-channel="controlBusChannel" output-channel="nullChannel"/>
|
||||
<control-bus input-channel="controlBusChannel" output-channel="nullChannel" use-registry="true"/>
|
||||
|
||||
</beans:beans>
|
||||
|
||||
@@ -16,19 +16,19 @@
|
||||
|
||||
package org.springframework.integration.config;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.store.MessageGroupStore;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -36,9 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Dave Syer
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@DirtiesContext
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
public class AggregatorWithMessageStoreParserTests {
|
||||
|
||||
@Autowired
|
||||
@@ -55,7 +54,6 @@ public class AggregatorWithMessageStoreParserTests {
|
||||
private MessageChannel controlBusChannel;
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void testAggregation() {
|
||||
input.send(createMessage("123", "id1", 3, 1, null));
|
||||
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(1);
|
||||
@@ -71,13 +69,15 @@ public class AggregatorWithMessageStoreParserTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DirtiesContext
|
||||
public void testExpiry() {
|
||||
input.send(createMessage("123", "id1", 3, 1, null));
|
||||
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(1);
|
||||
input.send(createMessage("456", "id1", 3, 2, null));
|
||||
assertThat(messageGroupStore.getMessageGroup("id1").size()).isEqualTo(2);
|
||||
this.controlBusChannel.send(new GenericMessage<Object>("@messageStore.expireMessageGroups(-10000)"));
|
||||
this.controlBusChannel.send(
|
||||
MessageBuilder.withPayload("messageStore.expireMessageGroups")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(-10000L))
|
||||
.build());
|
||||
assertThat(aggregatorBean
|
||||
.getAggregatedMessages().size()).as("One and only one message should have been aggregated")
|
||||
.isEqualTo(1);
|
||||
@@ -88,6 +88,7 @@ public class AggregatorWithMessageStoreParserTests {
|
||||
|
||||
private static <T> Message<T> createMessage(T payload, Object correlationId, int sequenceSize, int sequenceNumber,
|
||||
MessageChannel outputChannel) {
|
||||
|
||||
return MessageBuilder.withPayload(payload)
|
||||
.setCorrelationId(correlationId)
|
||||
.setSequenceSize(sequenceSize)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<queue />
|
||||
</channel>
|
||||
|
||||
<control-bus input-channel="input" output-channel="output">
|
||||
<control-bus input-channel="input" output-channel="output" use-registry="true">
|
||||
<poller fixed-rate="100" />
|
||||
</control-bus>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
|
||||
<control-bus input-channel="inputChannel" auto-startup="true"/>
|
||||
<control-bus input-channel="inputChannel" auto-startup="true" use-registry="true"/>
|
||||
|
||||
<inbound-channel-adapter id="adapter" channel="outputChannel" auto-startup="false" method="receive">
|
||||
<poller fixed-rate="1000"/>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<queue />
|
||||
</channel>
|
||||
|
||||
<control-bus input-channel="input" output-channel="output"/>
|
||||
<control-bus input-channel="input" output-channel="output" use-registry="true"/>
|
||||
|
||||
<poller default="true" fixed-rate="100"/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="false"/>
|
||||
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="false" use-registry="true"/>
|
||||
|
||||
<recipient-list-router id="simpleRouter" input-channel="routingChannelA"/>
|
||||
|
||||
|
||||
@@ -18,15 +18,17 @@ package org.springframework.integration.config.xml;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.router.RecipientListRouter.Recipient;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
@@ -36,17 +38,17 @@ import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.annotation.DirtiesContext.ClassMode;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Liujiong
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.1
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
public class ControlBusRecipientListRouterTests {
|
||||
|
||||
@@ -63,63 +65,79 @@ public class ControlBusRecipientListRouterTests {
|
||||
@Qualifier("routingChannelA")
|
||||
private MessageChannel channel;
|
||||
|
||||
@Before
|
||||
@BeforeEach
|
||||
public void aa() {
|
||||
context.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddRecipient() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel2','true')");
|
||||
Message<?> message = new GenericMessage<Integer>(1);
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel2", "true"))
|
||||
.build());
|
||||
Message<?> message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel2 = (PollableChannel) context.getBean("channel2");
|
||||
assertThat(chanel2.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel2.receive(0).getPayload()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddRecipientWithNullExpression() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel3')");
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel3"))
|
||||
.build());
|
||||
|
||||
Message<?> message = new GenericMessage<Integer>(1);
|
||||
Message<?> message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel3 = (PollableChannel) context.getBean("channel3");
|
||||
assertThat(chanel3.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel3.receive(0).getPayload()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveRecipient() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1')");
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel4')");
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.removeRecipient('channel4')");
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1"))
|
||||
.build());
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel4"))
|
||||
.build());
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.removeRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel4"))
|
||||
.build());
|
||||
|
||||
Message<?> message = new GenericMessage<Integer>(1);
|
||||
Message<?> message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel1 = (PollableChannel) context.getBean("channel1");
|
||||
PollableChannel chanel4 = (PollableChannel) context.getBean("channel4");
|
||||
assertThat(chanel1.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel1.receive(0).getPayload()).isEqualTo(1);
|
||||
assertThat(chanel4.receive(0)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveRecipientWithExpression() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1','true')");
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel5','true')");
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.removeRecipient('channel5','true')");
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1", "true"))
|
||||
.build());
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel5", "true"))
|
||||
.build());
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.removeRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel5", "true"))
|
||||
.build());
|
||||
|
||||
Message<?> message = new GenericMessage<Integer>(1);
|
||||
Message<?> message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel1 = (PollableChannel) context.getBean("channel1");
|
||||
PollableChannel chanel5 = (PollableChannel) context.getBean("channel5");
|
||||
assertThat(chanel1.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel1.receive(0).getPayload()).isEqualTo(1);
|
||||
assertThat(chanel5.receive(0)).isNull();
|
||||
}
|
||||
|
||||
@@ -127,8 +145,10 @@ public class ControlBusRecipientListRouterTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testGetRecipients() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.addRecipient('channel1')");
|
||||
messagingTemplate.send(input,
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.addRecipient")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("channel1"))
|
||||
.build());
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.getRecipients()");
|
||||
PollableChannel channel1 = (PollableChannel) context.getBean("channel1");
|
||||
Message<?> result = this.output.receive(0);
|
||||
@@ -138,29 +158,31 @@ public class ControlBusRecipientListRouterTests {
|
||||
|
||||
@Test
|
||||
public void testSetRecipients() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("channel6", "true");
|
||||
Message<?> message = MessageBuilder.withPayload("@'simpleRouter.handler'.setRecipientMappings(headers.recipientMap)").setHeader("recipientMap", map).build();
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.setRecipientMappings")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(map))
|
||||
.build();
|
||||
this.input.send(message);
|
||||
message = new GenericMessage<Integer>(1);
|
||||
message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel6 = (PollableChannel) context.getBean("channel6");
|
||||
assertThat(chanel6.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel6.receive(0).getPayload()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceRecipients() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
|
||||
messagingTemplate.convertAndSend(input, "@'simpleRouter.handler'.replaceRecipients('channel7=true')");
|
||||
Message<?> message = new GenericMessage<Integer>(1);
|
||||
Properties newMapping = new Properties();
|
||||
newMapping.setProperty("channel7", "true");
|
||||
this.input.send(
|
||||
MessageBuilder.withPayload("'simpleRouter.handler'.replaceRecipients")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
|
||||
.build());
|
||||
Message<?> message = new GenericMessage<>(1);
|
||||
channel.send(message);
|
||||
PollableChannel chanel7 = (PollableChannel) context.getBean("channel7");
|
||||
assertThat(chanel7.receive(0).getPayload().equals(1)).isTrue();
|
||||
assertThat(chanel7.receive(0).getPayload()).isEqualTo(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true"/>
|
||||
<control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true" use-registry="true"/>
|
||||
|
||||
<beans:bean id="service" class="org.springframework.integration.config.xml.ControlBusTests$Service" />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -89,10 +89,10 @@ import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.channel.NullChannel;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.integration.channel.interceptor.WireTap;
|
||||
import org.springframework.integration.config.ControlBusFactoryBean;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.config.EnableMessageHistory;
|
||||
import org.springframework.integration.config.EnablePublisher;
|
||||
import org.springframework.integration.config.ExpressionControlBusFactoryBean;
|
||||
import org.springframework.integration.config.GlobalChannelInterceptor;
|
||||
import org.springframework.integration.config.IntegrationConverter;
|
||||
import org.springframework.integration.config.SpelFunctionFactoryBean;
|
||||
@@ -454,15 +454,15 @@ public class EnableIntegrationTests {
|
||||
assertThat(message.getHeaders().get("foo")).isEqualTo("FOO");
|
||||
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate(this.controlBusChannel);
|
||||
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
|
||||
this.controlBusChannel.send(new GenericMessage<>("@pausable.start()"));
|
||||
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(true);
|
||||
this.controlBusChannel.send(new GenericMessage<>("@pausable.stop()"));
|
||||
assertThat(messagingTemplate.convertSendAndReceive("@pausable.isRunning()", Boolean.class)).isEqualTo(false);
|
||||
this.controlBusChannel.send(new GenericMessage<>("@pausable.pause()"));
|
||||
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(false);
|
||||
this.controlBusChannel.send(new GenericMessage<>("pausable.start"));
|
||||
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(true);
|
||||
this.controlBusChannel.send(new GenericMessage<>("pausable.stop"));
|
||||
assertThat(messagingTemplate.convertSendAndReceive("pausable.isRunning", Boolean.class)).isEqualTo(false);
|
||||
this.controlBusChannel.send(new GenericMessage<>("pausable.pause"));
|
||||
Object pausable = this.context.getBean("pausable");
|
||||
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isTrue();
|
||||
this.controlBusChannel.send(new GenericMessage<>("@pausable.resume()"));
|
||||
this.controlBusChannel.send(new GenericMessage<>("pausable.resume"));
|
||||
assertThat(TestUtils.getPropertyValue(pausable, "paused", Boolean.class)).isFalse();
|
||||
|
||||
Map<String, ServiceActivatingHandler> beansOfType =
|
||||
@@ -1113,8 +1113,8 @@ public class EnableIntegrationTests {
|
||||
@ServiceActivator(inputChannel = "controlBusChannel")
|
||||
@EndpointId("controlBusEndpoint")
|
||||
@Role("bar")
|
||||
public ExpressionControlBusFactoryBean controlBus() {
|
||||
return new ExpressionControlBusFactoryBean();
|
||||
public ControlBusFactoryBean controlBus() {
|
||||
return new ControlBusFactoryBean();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2015-2022 the original author or authors.
|
||||
* Copyright 2015-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.integration.support.management;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -26,17 +27,25 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.channel.AbstractMessageChannel;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.config.EnableIntegrationManagement;
|
||||
import org.springframework.integration.config.IntegrationManagementConfigurer;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.endpoint.AbstractMessageSource;
|
||||
import org.springframework.integration.handler.AbstractMessageHandler;
|
||||
import org.springframework.integration.handler.ControlBusMessageProcessor;
|
||||
import org.springframework.integration.router.RecipientListRouter;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -53,7 +62,7 @@ public class IntegrationManagementConfigurerTests {
|
||||
public void testDefaults() {
|
||||
DirectChannel channel = new DirectChannel();
|
||||
AbstractMessageHandler handler = new RecipientListRouter();
|
||||
AbstractMessageSource<?> source = new AbstractMessageSource<Object>() {
|
||||
AbstractMessageSource<?> source = new AbstractMessageSource<>() {
|
||||
|
||||
@Override
|
||||
public String getComponentType() {
|
||||
@@ -94,6 +103,68 @@ public class IntegrationManagementConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlBusIntegration() {
|
||||
try (ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(ControlBusEagerConfig.class)) {
|
||||
ControlBusCommandRegistry controlBusCommandRegistry =
|
||||
ctx.getBean(IntegrationContextUtils.CONTROL_BUS_COMMAND_REGISTRY_BEAN_NAME,
|
||||
ControlBusCommandRegistry.class);
|
||||
|
||||
Map<String, Map<ControlBusCommandRegistry.CommandMethod, String>> commands =
|
||||
controlBusCommandRegistry.getCommands();
|
||||
|
||||
assertThat(commands).containsKeys("errorChannel", "nullChannel", "taskScheduler", "channel",
|
||||
"_org.springframework.integration.errorLogger",
|
||||
"_org.springframework.integration.errorLogger.handler");
|
||||
|
||||
Map<ControlBusCommandRegistry.CommandMethod, String> commandMethodStringMap = commands.get("channel");
|
||||
|
||||
List<String> controlBusMethodForChannelBean =
|
||||
commandMethodStringMap.keySet()
|
||||
.stream()
|
||||
.map(ControlBusCommandRegistry.CommandMethod::getMethodName)
|
||||
.toList();
|
||||
|
||||
assertThat(controlBusMethodForChannelBean)
|
||||
.containsOnly("setLoggingEnabled", "isLoggingEnabled", "setShouldTrack");
|
||||
|
||||
Expression isLoggingEnabledCommand =
|
||||
controlBusCommandRegistry.getExpressionForCommand("nullChannel.isLoggingEnabled");
|
||||
|
||||
StandardEvaluationContext evaluationContext = IntegrationContextUtils.getEvaluationContext(ctx);
|
||||
|
||||
assertThat(isLoggingEnabledCommand.getValue(evaluationContext, boolean.class)).isFalse();
|
||||
|
||||
Expression setLoggingEnabledCommand =
|
||||
controlBusCommandRegistry.getExpressionForCommand("nullChannel.setLoggingEnabled", boolean.class);
|
||||
|
||||
setLoggingEnabledCommand.getValue(evaluationContext, new Object[] {true});
|
||||
|
||||
assertThat(isLoggingEnabledCommand.getValue(evaluationContext, boolean.class)).isTrue();
|
||||
|
||||
ControlBusMessageProcessor controlBusMessageProcessor = ctx.getBean(ControlBusMessageProcessor.class);
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() ->
|
||||
controlBusMessageProcessor.processMessage(new GenericMessage<>("nonSuchBean.command")))
|
||||
.withMessage("There is no registered bean for requested command: nonSuchBean");
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() ->
|
||||
controlBusMessageProcessor.processMessage(new GenericMessage<>("channel.noSuchCommand")))
|
||||
.withMessageStartingWith("No method 'noSuchCommand' found in bean 'bean 'channel'");
|
||||
|
||||
controlBusMessageProcessor.processMessage(
|
||||
MessageBuilder.withPayload("nullChannel.setLoggingEnabled")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(false))
|
||||
.build());
|
||||
|
||||
assertThat((Boolean) controlBusMessageProcessor.processMessage(
|
||||
new GenericMessage<>("nullChannel.isLoggingEnabled")))
|
||||
.isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableIntegration
|
||||
@EnableIntegrationManagement
|
||||
@@ -113,4 +184,21 @@ public class IntegrationManagementConfigurerTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableIntegration
|
||||
@EnableIntegrationManagement(loadControlBusCommands = "true", defaultLoggingEnabled = "false")
|
||||
public static class ControlBusEagerConfig {
|
||||
|
||||
@Bean
|
||||
MessageChannel channel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ControlBusMessageProcessor controlBusMessageProcessor(ControlBusCommandRegistry controlBusCommandRegistry) {
|
||||
return new ControlBusMessageProcessor(controlBusCommandRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -268,8 +268,26 @@ class GroovyIntegrationFlowDefinition {
|
||||
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
|
||||
* @see org.springframework.integration.handler.ExpressionCommandMessageProcessor* @see GenericEndpointSpec
|
||||
* @since 6.4
|
||||
*/
|
||||
GroovyIntegrationFlowDefinition controlBusOnRegistry(
|
||||
@DelegatesTo(value = GenericEndpointSpec<ServiceActivatingHandler>, strategy = Closure.DELEGATE_FIRST)
|
||||
@ClosureParams(value = SimpleType.class, options = 'org.springframework.integration.dsl.GenericEndpointSpec')
|
||||
Closure<?> endpointConfigurer = null) {
|
||||
|
||||
this.delegate.controlBusOnRegistry createConfigurerIfAny(endpointConfigurer)
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the {@code Control Bus} EI Pattern specific {@link MessageHandler} implementation
|
||||
* at the current {@link IntegrationFlow} chain position.
|
||||
* @param endpointConfigurer the {@link Consumer} to accept integration endpoint options.
|
||||
* @see GenericEndpointSpec
|
||||
* @deprecated in favor of {@link #controlBusOnRegistry}
|
||||
*/
|
||||
@Deprecated(since = '6.4', forRemoval = true)
|
||||
@SuppressWarnings('removal')
|
||||
GroovyIntegrationFlowDefinition controlBus(
|
||||
@DelegatesTo(value = GenericEndpointSpec<ServiceActivatingHandler>, strategy = Closure.DELEGATE_FIRST)
|
||||
@ClosureParams(value = SimpleType.class, options = 'org.springframework.integration.dsl.GenericEndpointSpec')
|
||||
|
||||
@@ -47,7 +47,10 @@ import org.springframework.util.ObjectUtils;
|
||||
* @author Gary Russell
|
||||
*
|
||||
* @since 2.0
|
||||
*
|
||||
* @deprecated in favor of {@link org.springframework.integration.handler.ControlBusMessageProcessor}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public class GroovyCommandMessageProcessor extends AbstractScriptExecutingMessageProcessor<Object>
|
||||
implements IntegrationPattern {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.integration.config.AbstractSimpleMessageHandlerFactoryBean;
|
||||
import org.springframework.integration.groovy.GroovyCommandMessageProcessor;
|
||||
import org.springframework.integration.handler.ServiceActivatingHandler;
|
||||
import org.springframework.integration.support.management.IntegrationManagedResource;
|
||||
import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
@@ -47,8 +46,12 @@ import org.springframework.util.CustomizableThreadCreator;
|
||||
* @author Artem Bilan
|
||||
* @author Stefan Reuter
|
||||
* @author Gary Russell
|
||||
*
|
||||
* @since 2.0
|
||||
*
|
||||
* @deprecated in favor of {@link org.springframework.integration.config.ControlBusFactoryBean}
|
||||
*/
|
||||
@Deprecated(since = "6.4", forRemoval = true)
|
||||
public class GroovyControlBusFactoryBean extends AbstractSimpleMessageHandlerFactoryBean<MessageHandler>
|
||||
implements BeanClassLoaderAware {
|
||||
|
||||
@@ -72,14 +75,16 @@ public class GroovyControlBusFactoryBean extends AbstractSimpleMessageHandlerFac
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
protected MessageHandler createHandler() {
|
||||
Binding binding = new ManagedBeansBinding(this.getBeanFactory());
|
||||
GroovyCommandMessageProcessor processor = new GroovyCommandMessageProcessor(binding,
|
||||
message -> {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("headers", message.getHeaders());
|
||||
return variables;
|
||||
});
|
||||
org.springframework.integration.groovy.GroovyCommandMessageProcessor processor =
|
||||
new org.springframework.integration.groovy.GroovyCommandMessageProcessor(binding,
|
||||
message -> {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("headers", message.getHeaders());
|
||||
return variables;
|
||||
});
|
||||
if (this.customizer != null) {
|
||||
processor.setCustomizer(this.customizer);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<xsd:element name="control-bus">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
[DEPRECATED in favor 'control-bus' component from core module]
|
||||
Control bus ('org.springframework.integration.groovy.config.GroovyControlBusFactoryBean') that accepts
|
||||
messages in the form of Groovy scripts. The scripts should be provided as
|
||||
String payload in incoming messages. The variable bindings will include any @ManagedResource, Lifecycle,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
def delayHandler = this.'testDelayer.handler'
|
||||
def delayedMessageCount = delayHandler.delayedMessageCount
|
||||
|
||||
println delayedMessageCount
|
||||
|
||||
assert 2 == delayedMessageCount
|
||||
|
||||
delayHandler.reschedulePersistedMessages()
|
||||
|
||||
return true
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:task="http://www.springframework.org/schema/task"
|
||||
xmlns:groovy="http://www.springframework.org/schema/integration/groovy"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/integration/groovy https://www.springframework.org/schema/integration/groovy/spring-integration-groovy.xsd
|
||||
http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<channel id="controlBusOutput">
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<channel id="output">
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<groovy:control-bus input-channel="controlBus" output-channel="controlBusOutput"/>
|
||||
|
||||
<beans:bean id="scheduler"
|
||||
class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
|
||||
<beans:property name="phase" value="1073741823"/>
|
||||
</beans:bean>
|
||||
|
||||
<delayer id="testDelayer" input-channel="delayerInput" output-channel="output"
|
||||
default-delay="1000"
|
||||
scheduler="scheduler"/>
|
||||
|
||||
</beans:beans>
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.groovy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
public class GroovyControlBusIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private MessageChannel controlBus;
|
||||
|
||||
@Autowired
|
||||
private PollableChannel output;
|
||||
|
||||
@Autowired
|
||||
private MessageChannel delayerInput;
|
||||
|
||||
@Autowired
|
||||
ThreadPoolTaskScheduler scheduler;
|
||||
|
||||
@Test
|
||||
public void testDelayerManagement() throws IOException {
|
||||
Message<String> testMessage = MessageBuilder.withPayload("test").build();
|
||||
this.delayerInput.send(testMessage);
|
||||
this.delayerInput.send(testMessage);
|
||||
|
||||
this.scheduler.destroy();
|
||||
// ensure the delayer did not release any messages
|
||||
assertThat(this.output.receive(500)).isNull();
|
||||
this.scheduler.afterPropertiesSet();
|
||||
|
||||
Resource scriptResource = new ClassPathResource("GroovyControlBusDelayerManagementTest.groovy", getClass());
|
||||
ScriptSource scriptSource = new ResourceScriptSource(scriptResource);
|
||||
Message<?> message = MessageBuilder.withPayload(scriptSource.getScriptAsString()).build();
|
||||
this.controlBus.send(message);
|
||||
|
||||
assertThat(this.output.receive(10000)).isNotNull();
|
||||
assertThat(this.output.receive(10000)).isNotNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.groovy;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import groovy.lang.Binding;
|
||||
import groovy.lang.MissingPropertyException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.integration.handler.MessageProcessor;
|
||||
import org.springframework.integration.scripting.DefaultScriptVariableGenerator;
|
||||
import org.springframework.integration.scripting.ScriptVariableGenerator;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Artem Bilan
|
||||
* @author Gunnar Hillert
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
public class GroovyScriptPayloadMessageProcessorTests {
|
||||
|
||||
private final AtomicInteger countHolder = new AtomicInteger();
|
||||
|
||||
private GroovyCommandMessageProcessor processor;
|
||||
|
||||
@Test
|
||||
public void testSimpleExecution() {
|
||||
int count = countHolder.getAndIncrement();
|
||||
Message<?> message = MessageBuilder.withPayload("headers.foo" + count).setHeader("foo" + count, "bar").build();
|
||||
processor = new GroovyCommandMessageProcessor();
|
||||
Object result = processor.processMessage(message);
|
||||
assertThat(result.toString()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleExecutionWithNewScript() {
|
||||
processor = new GroovyCommandMessageProcessor();
|
||||
Message<?> message = MessageBuilder.withPayload("headers.foo").setHeader("foo", "bar").build();
|
||||
Object result = processor.processMessage(message);
|
||||
assertThat(result.toString()).isEqualTo("bar");
|
||||
message = MessageBuilder.withPayload("headers.bar").setHeader("bar", "spam").build();
|
||||
result = processor.processMessage(message);
|
||||
assertThat(result.toString()).isEqualTo("spam");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleExecutionWithContext() {
|
||||
Message<?> message = MessageBuilder.withPayload("\"spam is $spam foo is $headers.foo\"")
|
||||
.setHeader("foo", "bar").build();
|
||||
ScriptVariableGenerator scriptVariableGenerator =
|
||||
new DefaultScriptVariableGenerator(Collections.singletonMap("spam", "bucket"));
|
||||
MessageProcessor<Object> processor = new GroovyCommandMessageProcessor(scriptVariableGenerator);
|
||||
Object result = processor.processMessage(message);
|
||||
assertThat(result.toString()).isEqualTo("spam is bucket foo is bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindingOverwrite() {
|
||||
Binding binding = new Binding() {
|
||||
|
||||
@Override
|
||||
public Object getVariable(String name) {
|
||||
throw new RuntimeException("intentional");
|
||||
}
|
||||
};
|
||||
Message<?> message = MessageBuilder.withPayload("foo").build();
|
||||
processor = new GroovyCommandMessageProcessor(binding);
|
||||
assertThatExceptionOfType(RuntimeException.class)
|
||||
.isThrownBy(() -> processor.processMessage(message))
|
||||
.withMessage("intentional");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindingOverwriteWithContext() {
|
||||
final String defaultValue = "default";
|
||||
Binding binding = new Binding() {
|
||||
|
||||
@Override
|
||||
public Object getVariable(String name) {
|
||||
try {
|
||||
return super.getVariable(name);
|
||||
}
|
||||
catch (MissingPropertyException e) {
|
||||
// ignore
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
ScriptVariableGenerator scriptVariableGenerator =
|
||||
new DefaultScriptVariableGenerator(Collections.singletonMap("spam", "bucket"));
|
||||
Message<?> message = MessageBuilder.withPayload("\"spam is $spam, foo is $foo\"").build();
|
||||
processor = new GroovyCommandMessageProcessor(binding, scriptVariableGenerator);
|
||||
Object result = processor.processMessage(message);
|
||||
assertThat(result.toString()).isEqualTo("spam is bucket, foo is default");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:groovy="http://www.springframework.org/schema/integration/groovy"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
http://www.springframework.org/schema/integration/groovy https://www.springframework.org/schema/integration/groovy/spring-integration-groovy.xsd
|
||||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<channel id="output">
|
||||
<queue/>
|
||||
</channel>
|
||||
|
||||
<beans:bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
|
||||
<beans:property name="scopes">
|
||||
<beans:map>
|
||||
<beans:entry key="thread" value="org.springframework.context.support.SimpleThreadScope"/>
|
||||
<beans:entry key="request" value="org.springframework.web.context.request.RequestScope"/>
|
||||
</beans:map>
|
||||
</beans:property>
|
||||
</beans:bean>
|
||||
|
||||
<groovy:control-bus input-channel="input" output-channel="output" send-timeout="100" order="1" auto-startup="true"
|
||||
customizer="groovyCustomizer">
|
||||
<groovy:request-handler-advice-chain>
|
||||
<beans:bean class="org.springframework.integration.groovy.config.GroovyControlBusTests$FooAdvice" />
|
||||
</groovy:request-handler-advice-chain>
|
||||
</groovy:control-bus>
|
||||
|
||||
<beans:bean id="service" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
|
||||
|
||||
<beans:bean id="threadScopedService"
|
||||
scope="thread"
|
||||
class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
|
||||
|
||||
<beans:bean id="requestScopedService"
|
||||
scope="request"
|
||||
class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service" />
|
||||
|
||||
<beans:bean id="nonManagedService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$NonManagedService" />
|
||||
|
||||
<beans:bean id="abstractService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service"
|
||||
abstract="true"/>
|
||||
|
||||
<beans:bean id="prototypeService" class="org.springframework.integration.groovy.config.GroovyControlBusTests$Service"
|
||||
scope="prototype"/>
|
||||
|
||||
<beans:bean id="groovyCustomizer"
|
||||
class="org.springframework.integration.groovy.config.GroovyControlBusTests$MyGroovyCustomizer"/>
|
||||
|
||||
</beans:beans>
|
||||
@@ -1,258 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.groovy.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import groovy.lang.GroovyObject;
|
||||
import org.junit.Before;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.MethodSorters;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanCreationNotAllowedException;
|
||||
import org.springframework.beans.factory.BeanIsAbstractException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.handler.advice.AbstractRequestHandlerAdvice;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedResource;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandlingException;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.scripting.groovy.GroovyObjectCustomizer;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Artem Bilan
|
||||
* @author Gary Russell
|
||||
* @author Gunnar Hillert
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class GroovyControlBusTests {
|
||||
|
||||
@Autowired
|
||||
private MessageChannel input;
|
||||
|
||||
@Autowired
|
||||
private PollableChannel output;
|
||||
|
||||
@Autowired
|
||||
private MyGroovyCustomizer groovyCustomizer;
|
||||
|
||||
private static volatile int adviceCalled;
|
||||
|
||||
@Before
|
||||
public void beforeTest() {
|
||||
adviceCalled = 0;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOperationOfControlBus() { // long is > 3
|
||||
this.groovyCustomizer.executed = false;
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload(
|
||||
"def result = service.convert('aardvark'); def foo = headers.foo; result+foo")
|
||||
.setHeader("foo", "bar")
|
||||
.build();
|
||||
this.input.send(message);
|
||||
assertThat(output.receive(0).getPayload()).isEqualTo("catbar");
|
||||
assertThat(output.receive(0)).isNull();
|
||||
assertThat(this.groovyCustomizer.executed).isTrue();
|
||||
assertThat(adviceCalled).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test //INT-2567
|
||||
public void testOperationWithCustomScope() {
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload("def result = threadScopedService.convert('testString')")
|
||||
.build();
|
||||
this.input.send(message);
|
||||
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
|
||||
}
|
||||
|
||||
@Test //INT-2567
|
||||
public void testFailOperationWithCustomScope() {
|
||||
try {
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload("def result = requestScopedService.convert('testString')")
|
||||
.build();
|
||||
this.input.send(message);
|
||||
fail("Expected BeanCreationException");
|
||||
}
|
||||
catch (Exception e) {
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause instanceof BeanCreationException)
|
||||
.as("Expected BeanCreationException, got " + cause.getClass() + ":" + cause
|
||||
.getMessage()).isTrue();
|
||||
assertThat(cause.getMessage().contains("requestScopedService")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test //INT-2567
|
||||
public void testOperationWithRequestCustomScope() {
|
||||
RequestContextHolder.setRequestAttributes(new MockRequestAttributes());
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload("def result = requestScopedService.convert('testString')")
|
||||
.build();
|
||||
this.input.send(message);
|
||||
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
|
||||
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test //INT-2567
|
||||
public void testFailOperationOnNonManagedComponent() {
|
||||
try {
|
||||
Message<?> message =
|
||||
MessageBuilder.withPayload("def result = nonManagedService.convert('testString')")
|
||||
.build();
|
||||
this.input.send(message);
|
||||
fail("Expected BeanCreationNotAllowedException");
|
||||
}
|
||||
catch (MessageHandlingException e) {
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause instanceof BeanCreationNotAllowedException)
|
||||
.as("Expected BeanCreationNotAllowedException, got " + cause.getClass() + ":" + cause.getMessage())
|
||||
.isTrue();
|
||||
assertThat(cause.getMessage().contains("nonManagedService")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test //INT-2631
|
||||
public void testFailOperationOnAbstractBean() {
|
||||
try {
|
||||
Message<?> message = MessageBuilder.withPayload("abstractService.convert('testString')").build();
|
||||
this.input.send(message);
|
||||
fail("Expected BeanIsAbstractException");
|
||||
}
|
||||
catch (MessageHandlingException e) {
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause instanceof BeanIsAbstractException)
|
||||
.as("Expected BeanIsAbstractException, got " + cause.getClass() + ":" + cause.getMessage())
|
||||
.isTrue();
|
||||
assertThat(cause.getMessage().contains("abstractService")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test //INT-2631
|
||||
public void testOperationOnPrototypeBean() {
|
||||
Message<?> message = MessageBuilder.withPayload("def result = prototypeService.convert('testString')").build();
|
||||
this.input.send(message);
|
||||
assertThat(output.receive(0).getPayload()).isEqualTo("cat");
|
||||
}
|
||||
|
||||
@ManagedResource
|
||||
public static class Service {
|
||||
|
||||
@ManagedOperation
|
||||
public String convert(String input) {
|
||||
return "cat";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NonManagedService {
|
||||
|
||||
public String convert(String input) {
|
||||
return "cat";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class MyGroovyCustomizer implements GroovyObjectCustomizer {
|
||||
|
||||
private volatile boolean executed;
|
||||
|
||||
@Override
|
||||
public void customize(GroovyObject goo) {
|
||||
this.executed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MockRequestAttributes implements RequestAttributes {
|
||||
|
||||
private final Map<String, Object> fakeRequest = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String name, int scope) {
|
||||
return fakeRequest.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value, int scope) {
|
||||
fakeRequest.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name, int scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getAttributeNames(int scope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerDestructionCallback(String name, Runnable callback, int scope) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveReference(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSessionMutex() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooAdvice extends AbstractRequestHandlerAdvice {
|
||||
|
||||
@Override
|
||||
protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
|
||||
adviceCalled++;
|
||||
return callback.execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.http.config;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.integration.http.management.ControlBusController;
|
||||
import org.springframework.integration.support.management.ControlBusCommandRegistry;
|
||||
|
||||
/**
|
||||
* Registers the {@link ControlBusController} bean.
|
||||
* <p>
|
||||
* Also calls {@link ControlBusCommandRegistry#setEagerInitialization(boolean)} with {@code true}
|
||||
* to load all the available commands in the application context.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 6.4
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class ControlBusControllerConfiguration {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(IntegrationGraphControllerRegistrar.class);
|
||||
|
||||
@Bean
|
||||
ControlBusController controlBusController(ControlBusCommandRegistry controlBusCommandRegistry,
|
||||
@Qualifier("mvcConversionService") FormattingConversionService conversionService) {
|
||||
|
||||
if (!HttpContextUtils.WEB_MVC_PRESENT && !HttpContextUtils.WEB_FLUX_PRESENT) {
|
||||
LOGGER.warn("The 'IntegrationGraphController' isn't registered with the application context because" +
|
||||
" there is no 'org.springframework.web.servlet.DispatcherServlet' or" +
|
||||
" 'org.springframework.web.reactive.DispatcherHandler' in the classpath.");
|
||||
return null;
|
||||
}
|
||||
|
||||
controlBusCommandRegistry.setEagerInitialization(true);
|
||||
|
||||
return new ControlBusController(controlBusCommandRegistry, conversionService);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.http.management;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.integration.context.IntegrationContextUtils;
|
||||
import org.springframework.integration.support.management.ControlBusCommandRegistry;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* The REST Controller to provide the management API for Control Bus pattern.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 4.3
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/control-bus")
|
||||
public class ControlBusController implements BeanFactoryAware, InitializingBean {
|
||||
|
||||
private final ControlBusCommandRegistry controlBusCommandRegistry;
|
||||
|
||||
private final FormattingConversionService conversionService;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
public ControlBusController(ControlBusCommandRegistry controlBusCommandRegistry,
|
||||
@Qualifier("mvcConversionService") FormattingConversionService conversionService) {
|
||||
|
||||
this.controlBusCommandRegistry = controlBusCommandRegistry;
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
this.evaluationContext = IntegrationContextUtils.getEvaluationContext(this.beanFactory);
|
||||
}
|
||||
|
||||
@GetMapping(name = "getCommands")
|
||||
public List<ControlBusBean> getCommands() {
|
||||
return this.controlBusCommandRegistry.getCommands()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map((beanEntry) -> createControlBusBean(beanEntry.getKey(), beanEntry.getValue()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping(name = "getCommandsForBean", path = "/{beanName}")
|
||||
public ControlBusBean getCommandsForBean(@PathVariable String beanName) {
|
||||
Map<ControlBusCommandRegistry.CommandMethod, String> commandsForBean =
|
||||
this.controlBusCommandRegistry.getCommands()
|
||||
.get(beanName);
|
||||
|
||||
return createControlBusBean(beanName, commandsForBean);
|
||||
}
|
||||
|
||||
private ControlBusBean createControlBusBean(String beanName,
|
||||
Map<ControlBusCommandRegistry.CommandMethod, String> commandsForBean) {
|
||||
|
||||
List<ControlBusCommand> commands =
|
||||
commandsForBean.keySet()
|
||||
.stream()
|
||||
.map(this::converControlBusCommand)
|
||||
.toList();
|
||||
|
||||
return new ControlBusBean(beanName, commands);
|
||||
}
|
||||
|
||||
private ControlBusCommand converControlBusCommand(ControlBusCommandRegistry.CommandMethod commandMethod) {
|
||||
return new ControlBusCommand(commandMethod.getBeanName() + '.' + commandMethod.getMethodName(),
|
||||
commandMethod.getDescription(),
|
||||
Arrays.asList(commandMethod.getParameterTypes()));
|
||||
}
|
||||
|
||||
@PostMapping(name = "invokeCommand", path = "/{command}")
|
||||
public Object invokeCommand(@PathVariable String command,
|
||||
@RequestBody(required = false) List<CommandArgument> arguments) {
|
||||
|
||||
Class<?>[] parameterTypes = new Class<?>[0];
|
||||
if (!CollectionUtils.isEmpty(arguments)) {
|
||||
parameterTypes = arguments.stream()
|
||||
.map(CommandArgument::parameterType)
|
||||
.toArray(Class<?>[]::new);
|
||||
}
|
||||
|
||||
Expression commandExpression = this.controlBusCommandRegistry.getExpressionForCommand(command, parameterTypes);
|
||||
|
||||
Object[] parameterValues = null;
|
||||
|
||||
if (!CollectionUtils.isEmpty(arguments)) {
|
||||
parameterValues = arguments.stream()
|
||||
.map((arg) -> this.conversionService.convert(arg.value, arg.parameterType))
|
||||
.toArray(Object[]::new);
|
||||
}
|
||||
|
||||
return commandExpression.getValue(this.evaluationContext, parameterValues);
|
||||
}
|
||||
|
||||
public record ControlBusBean(String beanName, List<ControlBusCommand> commands) {
|
||||
|
||||
}
|
||||
|
||||
public record ControlBusCommand(String command, String description, List<Class<?>> parameterTypes) {
|
||||
|
||||
}
|
||||
|
||||
public record CommandArgument(String value, Class<?> parameterType) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<int:channel id="cbChannel" />
|
||||
|
||||
<int:control-bus input-channel="cbChannel" />
|
||||
<int:control-bus input-channel="cbChannel" use-registry="true"/>
|
||||
|
||||
<int:gateway default-request-channel="cbChannel"
|
||||
service-interface="org.springframework.integration.ip.tcp.ClientModeControlBusTests$ControlBus"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<int:channel id="control" />
|
||||
|
||||
<int:control-bus input-channel="control" />
|
||||
<int:control-bus input-channel="control" use-registry="true"/>
|
||||
|
||||
<int:channel id="qux">
|
||||
<int:queue />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,36 +17,37 @@
|
||||
package org.springframework.integration.jmx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.core.MessagingTemplate;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.annotation.DirtiesContext.ClassMode;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
public class UpdateMappingsTests {
|
||||
|
||||
@@ -64,7 +65,10 @@ public class UpdateMappingsTests {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
control.send(new GenericMessage<String>("@myRouter.setChannelMapping('baz', 'qux')"));
|
||||
control.send(
|
||||
MessageBuilder.withPayload("myRouter.setChannelMapping")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of("baz", "qux"))
|
||||
.build());
|
||||
Message<?> message = MessageBuilder.withPayload("Hello, world!")
|
||||
.setHeader("routing.header", "baz").build();
|
||||
in.send(message);
|
||||
@@ -75,15 +79,28 @@ public class UpdateMappingsTests {
|
||||
public void testChangeRouterMappings() {
|
||||
MessagingTemplate messagingTemplate = new MessagingTemplate();
|
||||
messagingTemplate.setReceiveTimeout(1000);
|
||||
messagingTemplate.convertAndSend(control,
|
||||
"@'router.handler'.replaceChannelMappings('foo=bar \n baz=qux')");
|
||||
Map<?, ?> mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
|
||||
Properties newMapping = new Properties();
|
||||
newMapping.setProperty("foo", "bar");
|
||||
newMapping.setProperty("baz", "qux");
|
||||
messagingTemplate.send(control,
|
||||
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
|
||||
.build());
|
||||
Map<?, ?> mappings =
|
||||
messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
|
||||
assertThat(mappings).isNotNull();
|
||||
assertThat(mappings.size()).isEqualTo(2);
|
||||
assertThat(mappings.get("foo")).isEqualTo("bar");
|
||||
assertThat(mappings.get("baz")).isEqualTo("qux");
|
||||
messagingTemplate.convertAndSend(control,
|
||||
"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')");
|
||||
|
||||
newMapping = new Properties();
|
||||
newMapping.setProperty("foo", "qux");
|
||||
newMapping.setProperty("baz", "bar");
|
||||
messagingTemplate
|
||||
.send(control,
|
||||
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
|
||||
.build());
|
||||
mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
|
||||
assertThat(mappings.size()).isEqualTo(2);
|
||||
assertThat(mappings.get("baz")).isEqualTo("bar");
|
||||
@@ -98,13 +115,14 @@ public class UpdateMappingsTests {
|
||||
.getInstance("update.mapping.domain:type=MessageHandler,name=router,bean=endpoint"),
|
||||
null);
|
||||
assertThat(names.size()).isEqualTo(1);
|
||||
Map<String, String> map = new HashMap<String, String>();
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("foo", "bar");
|
||||
map.put("baz", "qux");
|
||||
Object[] params = new Object[] {map};
|
||||
this.server.invoke(names.iterator().next(), "setChannelMappings", params,
|
||||
new String[] {"java.util.Map"});
|
||||
Map<?, ?> mappings = messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
|
||||
Map<?, ?> mappings =
|
||||
messagingTemplate.convertSendAndReceive(control, "@'router.handler'.getChannelMappings()", Map.class);
|
||||
assertThat(mappings).isNotNull();
|
||||
assertThat(mappings.size()).isEqualTo(2);
|
||||
assertThat(mappings.get("foo")).isEqualTo("bar");
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<si:channel id="controlChannel"/>
|
||||
|
||||
<si:control-bus id = "cb" input-channel="controlChannel"/>
|
||||
<si:control-bus id = "cb" input-channel="controlChannel" use-registry="true"/>
|
||||
|
||||
<jmx:mbean-export id="integrationMbeanExporter" server="mbs" default-domain="tests.ControlBusParser"/>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<int:channel id="toControlBus" />
|
||||
|
||||
<int:control-bus input-channel="toControlBus" />
|
||||
<int:control-bus input-channel="toControlBus" use-registry="true"/>
|
||||
|
||||
<int-jmx:mbean-export id="integrationMbeanExporter"
|
||||
default-domain="self-destruct" />
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
selector-expression="payload.contains('stop')" />
|
||||
</int:recipient-list-router>
|
||||
|
||||
<int:control-bus input-channel="controlBusChannel" />
|
||||
<int:control-bus input-channel="controlBusChannel" use-registry="true"/>
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,7 @@ The `splitter` now supports a `discardChannel` configuration option.
|
||||
See xref:splitter.adoc[Splitter] for more information.
|
||||
|
||||
The Control Bus can now handle `Pausable` (extension of `Lifecycle`) operations.
|
||||
See xref:groovy.adoc#groovy-control-bus[Control Bus] for more information.
|
||||
See xref:control-bus.adoc[Control Bus] for more information.
|
||||
|
||||
The `Function<MessageGroup, Map<String, Object>>` strategy has been introduced for the aggregator component to merge and compute headers for output messages.
|
||||
See xref:aggregator.adoc#aggregator-api[Aggregator Programming Model] for more information.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -192,7 +192,7 @@ These operations can be invoked through a `Control Bus` command, as the followin
|
||||
[source,java]
|
||||
----
|
||||
Message<String> delayerReschedulingMessage =
|
||||
MessageBuilder.withPayload("@'delayer.handler'.reschedulePersistedMessages()").build();
|
||||
MessageBuilder.withPayload("'delayer.handler'.reschedulePersistedMessages").build();
|
||||
controlBusChannel.send(delayerReschedulingMessage);
|
||||
----
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,7 +18,7 @@ When all files are consumed, the remote fetch is attempted again, to pick up any
|
||||
IMPORTANT: When you deploy multiple instances of an application, we recommend a small `max-fetch-size`, to avoid one instance "`grabbing`" all the files and starving other instances.
|
||||
|
||||
Another use for `max-fetch-size` is if you want to stop fetching remote files but continue to process files that have already been fetched.
|
||||
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:groovy.adoc#groovy-control-bus[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
|
||||
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:control-bus.adoc[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
|
||||
If the poller is active when the property is changed, the change takes effect on the next poll.
|
||||
|
||||
Starting with version 5.1, the synchronizer can be provided with a `Comparator<FTPFile>`.
|
||||
|
||||
@@ -139,37 +139,3 @@ NOTE: Using `compilerConfiguration` does not automatically add an `ASTTransforma
|
||||
If you still need `CompileStatic`, you should manually add a `new ASTTransformationCustomizer(CompileStatic.class)` into the `CompilationCustomizers` of that custom `compilerConfiguration`.
|
||||
|
||||
NOTE: The Groovy compiler customization does not have any effect on the `refresh-check-delay` option, and reloadable scripts can be statically compiled, too.
|
||||
|
||||
[[groovy-control-bus]]
|
||||
== Control Bus
|
||||
|
||||
As described in (https://www.enterpriseintegrationpatterns.com/ControlBus.html[Enterprise Integration Patterns]), the idea behind the control bus is that you can use the same messaging system for monitoring and managing the components within the framework as is used for "`application-level`" messaging.
|
||||
In Spring Integration, we build upon the adapters described earlier so that you can send Messages as a means of invoking exposed operations.
|
||||
One option for those operations is Groovy scripts.
|
||||
The following example configures a Groovy script for the control bus:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<int-groovy:control-bus input-channel="operationChannel"/>
|
||||
----
|
||||
|
||||
The control bus has an input channel that can be accessed to invoke operations on the beans in the application context.
|
||||
|
||||
The Groovy control bus runs messages on the input channel as Groovy scripts.
|
||||
It takes a message, compiles the body to a script, customizes it with a `GroovyObjectCustomizer`, and runs it.
|
||||
The control bus' `MessageProcessor` exposes all beans in the application context that are annotated with `@ManagedResource` and implement Spring's `Lifecycle` interface or extend Spring's `CustomizableThreadCreator` base class (for example, several of the `TaskExecutor` and `TaskScheduler` implementations).
|
||||
|
||||
IMPORTANT: Be careful about using managed beans with custom scopes (such as 'request') in the Control Bus' command scripts, especially inside an asynchronous message flow.
|
||||
If `MessageProcessor` of the control bus cannot expose a bean from the application context, you may end up with some `BeansException` during the command script's run.
|
||||
For example, if a custom scope's context is not established, the attempt to get a bean within that scope triggers a `BeanCreationException`.
|
||||
|
||||
If you need to further customize the Groovy objects, you can also provide a reference to a bean that implements `GroovyObjectCustomizer` through the `customizer` attribute, as the following example shows:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<int-groovy:control-bus input-channel="input"
|
||||
output-channel="output"
|
||||
customizer="groovyCustomizer"/>
|
||||
|
||||
<beans:bean id="groovyCustomizer" class="org.foo.MyGroovyObjectCustomizer"/>
|
||||
----
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -97,7 +97,7 @@ In fact, it applies to every other router, including expression-based routers, s
|
||||
Any router that is a subclass of the `AbstractMappingMessageRouter` (which includes most framework-defined routers) is a dynamic router, because the `channelMapping` is defined at the `AbstractMappingMessageRouter` level.
|
||||
That map's setter method is exposed as a public method along with the 'setChannelMapping' and 'removeChannelMapping' methods.
|
||||
These let you change, add, and remove router mappings at runtime, as long as you have a reference to the router itself.
|
||||
It also means that you could expose these same configuration options through JMX (see xref:jmx.adoc[JMX Support]) or the Spring Integration control bus (see xref:groovy.adoc#groovy-control-bus[Control Bus]) functionality.
|
||||
It also means that you could expose these same configuration options through JMX (see xref:jmx.adoc[JMX Support]) or the Spring Integration control bus (see xref:control-bus.adoc[Control Bus]) functionality.
|
||||
|
||||
IMPORTANT: Falling back to the channel key as the channel name is flexible and convenient.
|
||||
However, if you don't trust the message creator, a malicious actor (who has knowledge of the system) could create a message that is routed to an unexpected channel.
|
||||
@@ -109,7 +109,7 @@ You may therefore wish to disable this feature (set the `channelKeyFallback` pro
|
||||
|
||||
One way to manage the router mappings is through the https://www.enterpriseintegrationpatterns.com/ControlBus.html[control bus] pattern, which exposes a control channel to which you can send control messages to manage and monitor Spring Integration components, including routers.
|
||||
|
||||
NOTE: For more information about the control bus, see xref:groovy.adoc#groovy-control-bus[Control Bus].
|
||||
NOTE: For more information about the control bus, see xref:control-bus.adoc[Control Bus].
|
||||
|
||||
Typically, you would send a control message asking to invoke a particular operation on a particular managed component (such as a router).
|
||||
The following managed operations (methods) are specific to changing the router resolution process:
|
||||
@@ -125,15 +125,19 @@ The following methods let you do so:
|
||||
|
||||
* `public Map<String, String>getChannelMappings()`: Returns the current mappings.
|
||||
* `public void replaceChannelMappings(Properties channelMappings)`: Updates the mappings.
|
||||
Note that the `channelMappings` parameter is a `Properties` object.
|
||||
This arrangement lets a control bus command use the built-in `StringToPropertiesConverter`, as the following example shows:
|
||||
Note that the `channelMappings` parameter is a `Properties` object, so this has to be added to the respective `IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS` header:
|
||||
|
||||
[source]
|
||||
----
|
||||
"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')"
|
||||
Properties newMapping = new Properties();
|
||||
newMapping.setProperty("foo", "bar");
|
||||
newMapping.setProperty("baz", "qux");
|
||||
Message<?> replaceChannelMappingsCommandMessage =
|
||||
MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
|
||||
.setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
|
||||
.build();
|
||||
----
|
||||
|
||||
Note that each mapping is separated by a newline character (`\n`).
|
||||
For programmatic changes to the map, we recommend that you use the `setChannelMappings` method, due to type-safety concerns.
|
||||
`replaceChannelMappings` ignores keys or values that are not `String` objects.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -315,7 +315,7 @@ When all files are consumed, the remote fetch is attempted again, to pick up any
|
||||
IMPORTANT: When you deploy multiple instances of an application, we recommend a small `max-fetch-size`, to avoid one instance "`grabbing`" all the files and starving other instances.
|
||||
|
||||
Another use for `max-fetch-size` is if you want to stop fetching remote files but continue to process files that have already been fetched.
|
||||
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:groovy.adoc#groovy-control-bus[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
|
||||
Setting the `maxFetchSize` property on the `MessageSource` (programmatically, with JMX, or with a xref:control-bus.adoc[control bus]) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched.
|
||||
If the poller is active when the property is changed, the change takes effect on the next poll.
|
||||
|
||||
The synchronizer can be provided with a `Comparator<SmbFile>`.
|
||||
|
||||
@@ -16,6 +16,13 @@ In general the project has been moved to the latest dependency versions.
|
||||
[[x6.4-new-components]]
|
||||
=== New Components
|
||||
|
||||
The new Control Bus interaction model is implemented in the `ControlBusCommandRegistry`.
|
||||
A new `ControlBusFactoryBean` class is recommended to be used instead of deprecated `ExpressionControlBusFactoryBean`.
|
||||
See xref:control-bus.adoc[Control Bus] for more information.
|
||||
|
||||
Also, a `ControlBusController` (together with an `@EnableControlBusController`) is introduced for managing exposed commands by the mentioned `ControlBusCommandRegistry`.
|
||||
See xref:http.adoc[HTTP Support] for more information.
|
||||
|
||||
[[x6.4-general]]
|
||||
=== General Changes
|
||||
|
||||
@@ -34,13 +41,22 @@ The byte array handling for serialized message is fully deferred to JDBC driver.
|
||||
|
||||
The `LockRepository.delete()` method return the result of removing ownership of a distributed lock.
|
||||
And the `JdbcLockRegistry.JdbcLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
|
||||
See xref:jdbc.adoc[JDBC Support] for more information.
|
||||
|
||||
[[x6.4-zeromq-changes]]
|
||||
=== ZeroMQ Changes
|
||||
|
||||
The outbound component `ZeroMqMessageHandler` (and respective API) can now bind a TCP port instead of connecting to a given URL.
|
||||
See xref:zeromq.adoc[ZeroMQ Support] for more information.
|
||||
|
||||
[[x6.4-redis-changes]]
|
||||
=== Redis Changes
|
||||
|
||||
Instead of throwing `IllegalStateException`, the `RedisLockRegistry.RedisLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
|
||||
Instead of throwing `IllegalStateException`, the `RedisLockRegistry.RedisLock.unlock()` method throws `ConcurrentModificationException` if the ownership of the lock is expired.
|
||||
See xref:redis.adoc[Redis Support] for more information.
|
||||
|
||||
[[x6.4-groovy-changes]]
|
||||
=== Groovy Changes
|
||||
|
||||
The `ControlBusFactoryBean` (and respective `<int-groovy:control-bus>` XML tag) has been deprecated (for removal) in favor of new introduced `ControlBusFactoryBean` based on a new model implemented in the `ControlBusCommandRegistry`.
|
||||
See xref:control-bus.adoc[Control Bus] for more information.
|
||||
Reference in New Issue
Block a user