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">
|
||||
|
||||
Reference in New Issue
Block a user