From c2e66b98f5c7bcc13f5d750a21b2aafa2a46aa0e Mon Sep 17 00:00:00 2001 From: Jiandong Ma Date: Tue, 2 Jun 2026 15:50:52 +0800 Subject: [PATCH] Introduce `enableAnnotations` in global `IntegrationProperties` the value default to true but can be set to false via: 1. adding one line config `spring.integration.annotations.enable=false` into the classpath file `spring.integration.properties` 2. defining a new `integrationGlobalProperties` bean with `setEnableAnnotations(false)`. Also add a test to demonstrate the performance improvement when turnoff the annotation processing. see: `MessagingAnnotationsConfigurationTests.testEnableVersusDisablePerformance` Signed-off-by: Jiandong Ma --- .../MessagingAnnotationBeanPostProcessor.java | 16 ++ .../MessagingAnnotationPostProcessor.java | 15 ++ .../context/IntegrationProperties.java | 36 ++++- ...essagingAnnotationsConfigurationTests.java | 149 ++++++++++++++++++ .../CustomMessagingAnnotationTests.java | 4 + .../META-INF/spring.integration.properties | 1 + 6 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/config/MessagingAnnotationsConfigurationTests.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationBeanPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationBeanPostProcessor.java index 86e147eb61f..ea76deef73b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationBeanPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationBeanPostProcessor.java @@ -25,6 +25,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -41,6 +43,8 @@ import org.springframework.integration.annotation.EndpointId; import org.springframework.integration.annotation.Role; import org.springframework.integration.config.annotation.MethodAnnotationPostProcessor; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.context.IntegrationProperties; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.integration.util.MessagingAnnotationUtils; import org.springframework.util.Assert; @@ -51,8 +55,10 @@ /** * An infrastructure {@link BeanPostProcessor} implementation that processes method-level * messaging annotations (@Transformer, @Splitter, @Router, @Filter etc.) on bean methods. + *

Short-circuits processing if messaging annotations is disabled via {@link IntegrationProperties}. * * @author Artem Bilan + * @author Jiandong Ma * * @since 6.2 */ @@ -70,6 +76,8 @@ public class MessagingAnnotationBeanPostProcessor private volatile boolean initialized; + private @Nullable Boolean enableAnnotations = null; + public MessagingAnnotationBeanPostProcessor( Map, MethodAnnotationPostProcessor> postProcessors) { @@ -86,6 +94,14 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { Assert.notNull(this.beanFactory, "BeanFactory must not be null"); + if (this.enableAnnotations == null) { + IntegrationProperties integrationProperties = IntegrationContextUtils.getIntegrationProperties(this.beanFactory); + this.enableAnnotations = integrationProperties.isEnableAnnotations(); + } + if (!this.enableAnnotations) { + return bean; + } + Class beanClass = AopUtils.getTargetClass(bean); // the set will hold records of prior class scans and indicate if no messaging annotations were found diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationPostProcessor.java index bc84fda3f1f..b528117ded1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingAnnotationPostProcessor.java @@ -46,18 +46,22 @@ import org.springframework.integration.annotation.Splitter; import org.springframework.integration.annotation.Transformer; import org.springframework.integration.config.annotation.MethodAnnotationPostProcessor; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.context.IntegrationProperties; import org.springframework.integration.util.MessagingAnnotationUtils; import org.springframework.util.CollectionUtils; /** * A {@link BeanDefinitionRegistryPostProcessor} implementation that processes method-level * messaging annotations (@Transformer, @Splitter, @Router, @Filter etc.) on @Bean configuration methods. + *

Short-circuits processing if messaging annotations is disabled via {@link IntegrationProperties}. * * @author Mark Fisher * @author Marius Bogoevici * @author Artem Bilan * @author Gary Russell * @author Rick Hogge + * @author Jiandong Ma */ public class MessagingAnnotationPostProcessor implements BeanDefinitionRegistryPostProcessor { @@ -69,10 +73,21 @@ public class MessagingAnnotationPostProcessor implements BeanDefinitionRegistryP @SuppressWarnings("NullAway.Init") private ConfigurableListableBeanFactory beanFactory; + private @Nullable Boolean enableAnnotations = null; + @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { this.registry = registry; + var beanFactory = (ConfigurableListableBeanFactory) this.registry; + if (this.enableAnnotations == null) { + IntegrationProperties integrationProperties = IntegrationContextUtils.getIntegrationProperties(beanFactory); + this.enableAnnotations = integrationProperties.isEnableAnnotations(); + } + if (!this.enableAnnotations) { + return; + } + this.postProcessors.put(Filter.class, new FilterAnnotationPostProcessor()); this.postProcessors.put(Router.class, new RouterAnnotationPostProcessor()); this.postProcessors.put(Transformer.class, new TransformerAnnotationPostProcessor()); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java index 4efb0e9ffac..f47494a596c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/context/IntegrationProperties.java @@ -27,7 +27,7 @@ /** * Utility class to encapsulate infrastructure Integration properties constants and their default values. - * The default values can be overridden by the {@code META-INF/spring.integration.properties} with this entries + * The default values can be overridden by the {@code META-INF/spring.integration.properties} with these entries * (includes their default values): *

* * @author Artem Bilan + * @author Jiandong Ma * * @since 3.0 */ @@ -119,6 +121,12 @@ public final class IntegrationProperties { */ public static final String ENDPOINTS_DEFAULT_TIMEOUT = INTEGRATION_PROPERTIES_PREFIX + "endpoints.defaultTimeout"; + /** + * Specifies whether to enable messaging annotations processing. + * @since 7.1 + */ + public static final String ENABLE_ANNOTATIONS = INTEGRATION_PROPERTIES_PREFIX + "annotations.enable"; + private static final Properties DEFAULTS; private boolean channelsAutoCreate = true; @@ -141,6 +149,8 @@ public final class IntegrationProperties { private long endpointsDefaultTimeout = IntegrationContextUtils.DEFAULT_TIMEOUT; + private boolean enableAnnotations = true; + @Nullable private volatile Properties properties; @@ -318,6 +328,25 @@ public long getEndpointsDefaultTimeout() { */ public void setEndpointsDefaultTimeout(long endpointsDefaultTimeout) { this.endpointsDefaultTimeout = endpointsDefaultTimeout; + this.properties = null; + } + + /** + * Return the value of {@link #ENABLE_ANNOTATIONS} option. + * @return the value of {@link #ENABLE_ANNOTATIONS} option. + * @since 7.1 + */ + public boolean isEnableAnnotations() { + return this.enableAnnotations; + } + + /** + * Configure a value for {@link #ENABLE_ANNOTATIONS} option. + * @param enableAnnotations the value for {@link #ENABLE_ANNOTATIONS} option. + */ + public void setEnableAnnotations(boolean enableAnnotations) { + this.enableAnnotations = enableAnnotations; + this.properties = null; } /** @@ -340,6 +369,7 @@ public Properties toProperties() { props.setProperty(ENDPOINTS_NO_AUTO_STARTUP, StringUtils.arrayToCommaDelimitedString(this.noAutoStartupEndpoints)); props.setProperty(ENDPOINTS_DEFAULT_TIMEOUT, "" + this.endpointsDefaultTimeout); + props.setProperty(ENABLE_ANNOTATIONS, "" + this.enableAnnotations); this.properties = props; } @@ -378,7 +408,9 @@ public static IntegrationProperties parse(Properties properties) { (value) -> integrationProperties.setNoAutoStartupEndpoints( StringUtils.commaDelimitedListToStringArray(value))) .acceptIfHasText(properties.getProperty(ENDPOINTS_DEFAULT_TIMEOUT), - (value) -> integrationProperties.setEndpointsDefaultTimeout(Long.parseLong(value))); + (value) -> integrationProperties.setEndpointsDefaultTimeout(Long.parseLong(value))) + .acceptIfHasText(properties.getProperty(ENABLE_ANNOTATIONS), + (value) -> integrationProperties.setEnableAnnotations(Boolean.parseBoolean(value))); return integrationProperties; } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/MessagingAnnotationsConfigurationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/MessagingAnnotationsConfigurationTests.java new file mode 100644 index 00000000000..c7a50c2df40 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/MessagingAnnotationsConfigurationTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2026-present 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.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.RetryingTest; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.integration.context.IntegrationProperties; +import org.springframework.util.StopWatch; + +/** + * @author Jiandong Ma + */ +class MessagingAnnotationsConfigurationTests { + + @ParameterizedTest + @ValueSource(classes = {DefaultConfig.class, EnableAnnotationsConfig.class}) + void testEnableAnnotations(Class configClass) { + try (var context = new AnnotationConfigApplicationContext(configClass)) { + + var integrationProperties = IntegrationContextUtils.getIntegrationProperties(context); + + Assertions.assertThat(integrationProperties) + .extracting(IntegrationProperties::isEnableAnnotations) + .isEqualTo(true); + + String serviceActivator = ServiceActivatorAnnotatedBean.class.getName() + ".handlePayload.serviceActivator"; + String serviceActivatorHandler = serviceActivator + ".handler"; + + Assertions.assertThat(context.containsBean(serviceActivator)).isTrue(); + Assertions.assertThat(context.containsBean(serviceActivatorHandler)).isTrue(); + } + } + + @Test + void testDisableAnnotations() { + try (var context = new AnnotationConfigApplicationContext(DisableAnnotationsConfig.class)) { + + var integrationProperties = IntegrationContextUtils.getIntegrationProperties(context); + + Assertions.assertThat(integrationProperties) + .extracting(IntegrationProperties::isEnableAnnotations) + .isEqualTo(false); + + String serviceActivator = ServiceActivatorAnnotatedBean.class.getName() + ".handlePayload.serviceActivator"; + String serviceActivatorHandler = serviceActivator + ".handler"; + + Assertions.assertThat(context.containsBean(serviceActivator)).isFalse(); + Assertions.assertThat(context.containsBean(serviceActivatorHandler)).isFalse(); + } + } + + @RetryingTest(10) + void testEnableVersusDisablePerformance() { + // JVM warmup + new AnnotationConfigApplicationContext(EnableAnnotationsConfig.class); + new AnnotationConfigApplicationContext(DisableAnnotationsConfig.class); + + StopWatch stopWatch = new StopWatch(); + + stopWatch.start(); + new AnnotationConfigApplicationContext(EnableAnnotationsConfig.class); + stopWatch.stop(); + long enableAnnotationsStartupTime = stopWatch.lastTaskInfo().getTimeMillis(); // ~= 40ms + + stopWatch.start(); + new AnnotationConfigApplicationContext(DisableAnnotationsConfig.class); + stopWatch.stop(); + long disableAnnotationsStartupTime = stopWatch.lastTaskInfo().getTimeMillis(); // ~= 25ms + + Assertions.assertThat(enableAnnotationsStartupTime).isGreaterThan(disableAnnotationsStartupTime); + } + + @EnableIntegration + @Import({ChannelConfig.class, ServiceActivatorAnnotatedBean.class}) + static class DefaultConfig { + + } + + @EnableIntegration + @Import({ChannelConfig.class, ServiceActivatorAnnotatedBean.class}) + static class EnableAnnotationsConfig { + + @Bean(name = IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME) + IntegrationProperties integrationProperties() { + var integrationProperties = IntegrationProperties.DEFAULT_INSTANCE; + integrationProperties.setEnableAnnotations(true); + return integrationProperties; + } + + } + + @EnableIntegration + @Import({ChannelConfig.class, ServiceActivatorAnnotatedBean.class}) + static class DisableAnnotationsConfig { + + @Bean(name = IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME) + IntegrationProperties integrationProperties() { + var integrationProperties = IntegrationProperties.DEFAULT_INSTANCE; + integrationProperties.setEnableAnnotations(false); + return integrationProperties; + } + + } + + @Configuration + static class ChannelConfig { + + @Bean + DirectChannel inputChannel() { + return new DirectChannel(); + } + + } + + static class ServiceActivatorAnnotatedBean { + + @ServiceActivator(inputChannel = "inputChannel") + void handlePayload(String payload) { + + } + + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java index c0536193f96..3ddcc3ae2b7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/annotation/CustomMessagingAnnotationTests.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.function.Supplier; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -66,6 +67,9 @@ */ @SpringJUnitConfig @DirtiesContext +@Disabled("BeanCurrentlyInCreationException: Error creating bean with name 'integrationGlobalProperties': " + + "Requested bean is currently in creation: Is there an unresolvable circular reference " + + "or an asynchronous initialization dependency?") public class CustomMessagingAnnotationTests { @Autowired(required = false) diff --git a/spring-integration-core/src/test/resources/META-INF/spring.integration.properties b/spring-integration-core/src/test/resources/META-INF/spring.integration.properties index 0298eb27188..014f1139b3d 100644 --- a/spring-integration-core/src/test/resources/META-INF/spring.integration.properties +++ b/spring-integration-core/src/test/resources/META-INF/spring.integration.properties @@ -5,3 +5,4 @@ spring.integration.taskScheduler.poolSize=20 spring.integration.messagingTemplate.throwExceptionOnLateReply=true spring.integration.endpoints.noAutoStartup=fooService*,stringSupplierEndpoint spring.integration.endpoints.defaultTimeout=45000 +spring.integration.annotations.enable=true