Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -51,8 +55,10 @@
/**
* An infrastructure {@link BeanPostProcessor} implementation that processes method-level
* messaging annotations (@Transformer, @Splitter, @Router, @Filter etc.) on bean methods.
* <p> Short-circuits processing if messaging annotations is disabled via {@link IntegrationProperties}.
*
* @author Artem Bilan
* @author Jiandong Ma
*
* @since 6.2
*/
Expand All @@ -70,6 +76,8 @@ public class MessagingAnnotationBeanPostProcessor

private volatile boolean initialized;

private @Nullable Boolean enableAnnotations = null;

public MessagingAnnotationBeanPostProcessor(
Map<Class<? extends Annotation>, MethodAnnotationPostProcessor<?>> postProcessors) {

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p> 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 {

Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
* <ul>
* <li> {@code spring.integration.channels.autoCreate=true}
Expand All @@ -40,9 +40,11 @@
* <li> {@code spring.integration.channels.error.requireSubscribers=true}
* <li> {@code spring.integration.channels.error.ignoreFailures=true}
* <li> {@code spring.integration.endpoints.defaultTimeout=30000}
* <li> {@code spring.integration.annotations.enable=true}
* </ul>
*
* @author Artem Bilan
* @author Jiandong Ma
*
* @since 3.0
*/
Expand Down Expand Up @@ -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;
Expand All @@ -141,6 +149,8 @@ public final class IntegrationProperties {

private long endpointsDefaultTimeout = IntegrationContextUtils.DEFAULT_TIMEOUT;

private boolean enableAnnotations = true;

@Nullable
private volatile Properties properties;

Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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) {

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading