From 39bb09012a05919fb356eb44132d1bdaac8d1465 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 1 Nov 2022 10:28:07 -0400 Subject: [PATCH] GH-3926: BeanNameGenerator for @MessagingGateway (#3927) * GH-3926: BeanNameGenerator for @MessagingGateway Fixes https://github.com/spring-projects/spring-integration/issues/3926 Current approach for generated bean name in the `MessagingGatewayRegistrar` is to decapitalize simple class name, which is similar to standard `AnnotationBeanNameGenerator` * Make logic in the `MessagingGatewayRegistrar` based on the provided `BeanNameGenerator` * Expose an `@IntegrationComponentScan.nameGenerator()` attribute to allow to customize default bean name generation strategy * Introduce `IntegrationConfigUtils.annotationBeanNameGenerator()` to take a provided `AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR` singleton or fallback to the `AnnotationBeanNameGenerator.INSTANCE` * Use this utility in the `IntegrationComponentScanRegistrar` if no custom strategy is provided in the `@IntegrationComponentScan` * Use same util from the `GatewayParser` since there is no custom naming strategy configuration * Some other current Java level refactoring in the `IntegrationComponentScanRegistrar` and `MessagingGatewayRegistrar` * * Meta-annotate `@MessagingGateway` with a `@MessageEndpoint` * Alias `@MessageEndpoint.value()` with a `@Component.value()` * Alias `@MessagingGateway.name()` with a `@MessageEndpoint.value()` * * Remove unused imports * * Replace `MessagingGatewayRegistrar` parsing logic for annotation configuration directly by the `GatewayProxyInstantiationPostProcessor` and `AnnotationGatewayProxyFactoryBean`. * The `MessagingGatewayRegistrar` logic has been migrated back to the `GatewayParser`, but only with an XML-relevant parts * Change the logic of the `IntegrationComponentScanRegistrar` to rely on a `ClassPathBeanDefinitionScanner` and its `scan()` functionality since this is all what we need to trigger a `GatewayProxyInstantiationPostProcessor` for scanned components * Fix `GatewayProxyInstantiationPostProcessor` to call an `afterPropertiesSet()` as well * Add a `value()` alias attribute for the `@MessagingGateway` to satisfy a name resolution from a `@Component` * Replace annotation chain resolution logic by new `MessagingAnnotationUtils.resolveMergedAttribute()` API * Use `MergedAnnotations` API in the `AnnotationGatewayProxyFactoryBean` to preserve a logic for attribute resolution from the annotation hierarchy * Add expression resolution for attribute values in the `AnnotationGatewayProxyFactoryBean` * Make a `GatewayInterfaceTests.CustomBeanNameGenerator` as an `AnnotationBeanNameGenerator` extension to satisfy the test logic expectations: no custom name if explicit is present * Clean up some doc typos after merge conflict The fix in this commit essentially resolves some old JIRA ticket: https://jira.spring.io/browse/INT-4558 * * Remove redundant `MessagingAnnotationUtils.resolveMergedAttribute()` * Optimize the logic in the `AnnotationGatewayProxyFactoryBean` around annotation attributes to plain annotation - no adaptation to maps --- .../annotation/IntegrationComponentScan.java | 16 +- .../annotation/MessageEndpoint.java | 6 +- .../annotation/MessagingGateway.java | 23 +- ...atewayProxyInstantiationPostProcessor.java | 29 +- .../IntegrationComponentScanRegistrar.java | 106 +++---- .../config/IntegrationConfigUtils.java | 18 ++ .../config/MessagingGatewayRegistrar.java | 274 ------------------ .../integration/config/xml/GatewayParser.java | 169 ++++++++++- .../AnnotationGatewayProxyFactoryBean.java | 87 ++++-- .../configuration/EnableIntegrationTests.java | 10 +- .../EnableComponentScanTests.java | 14 +- .../gateway/GatewayInterfaceTests.java | 31 +- src/reference/asciidoc/gateway.adoc | 4 + src/reference/asciidoc/whats-new.adoc | 5 +- 14 files changed, 394 insertions(+), 398 deletions(-) delete mode 100644 spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java index 6f4e0cfda6..3acf78723d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2022 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. @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; @@ -105,4 +106,17 @@ public @interface IntegrationComponentScan { */ Filter[] excludeFilters() default { }; + + /** + * The {@link BeanNameGenerator} class to be used for naming detected Spring Integration components. + *

The default value of the {@link BeanNameGenerator} interface itself indicates + * that the scanner used to process this {@link IntegrationComponentScan} annotation should + * use its inherited bean name generator, e.g. the default + * {@link org.springframework.context.annotation.AnnotationBeanNameGenerator} + * or any custom instance supplied to the application context at bootstrap time. + * @since 6.0 + * @see org.springframework.context.annotation.ComponentScan#nameGenerator() + */ + Class nameGenerator() default BeanNameGenerator.class; + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessageEndpoint.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessageEndpoint.java index 8e26ed51d3..37221e247c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessageEndpoint.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessageEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -23,6 +23,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; /** @@ -30,6 +31,7 @@ import org.springframework.stereotype.Component; * Message Endpoint. * * @author Mark Fisher + * @author Artem Bilan */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -41,9 +43,9 @@ public @interface MessageEndpoint { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. - * * @return the suggested component name, if any */ + @AliasFor(annotation = Component.class) String value() default ""; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java index 53419dab49..d05a16f273 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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,12 +21,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.stereotype.Indexed; +import org.springframework.core.annotation.AliasFor; /** * A stereotype annotation to provide an Integration Messaging Gateway Proxy - * ({@code }) as an abstraction over the messaging API. The target - * application's business logic may be completely unaware of the Spring Integration + * as an abstraction over the messaging API. The target application's + * business logic may be completely unaware of the Spring Integration * API, with the code interacting only via the interface. *

* Important: The {@link IntegrationComponentScan} annotation is required along with @@ -41,17 +41,28 @@ import org.springframework.stereotype.Indexed; * @since 4.0 * * @see IntegrationComponentScan + * @see MessageEndpoint */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Indexed +@MessageEndpoint public @interface MessagingGateway { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any + * @since 6.0 */ + @AliasFor(annotation = MessageEndpoint.class) + String value() default ""; + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + @AliasFor(annotation = MessageEndpoint.class, attribute = "value") String name() default ""; /** @@ -90,7 +101,7 @@ public @interface MessagingGateway { /** * Allows to specify how long this gateway will wait for the reply {@code Message} - * before returning. By default it will wait indefinitely. {@code null} is returned if + * before returning. By default, it will wait indefinitely. {@code null} is returned if * the gateway times out. Value is specified in milliseconds; it can be a simple long * value or a SpEL expression; array variable #args is available. * @return the suggested timeout in milliseconds, if any diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java index 07d596b05f..6d743e2bcc 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java @@ -18,10 +18,12 @@ package org.springframework.integration.config; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.ScannedGenericBeanDefinition; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.integration.annotation.MessagingGateway; import org.springframework.integration.gateway.AnnotationGatewayProxyFactoryBean; @@ -54,20 +56,23 @@ class GatewayProxyInstantiationPostProcessor implements @Override public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { - if (beanClass.isInterface() - && AnnotatedElementUtils.hasAnnotation(beanClass, MessagingGateway.class) - && this.registry.getBeanDefinition(beanName) instanceof AnnotatedGenericBeanDefinition) { + if (beanClass.isInterface() && AnnotatedElementUtils.hasAnnotation(beanClass, MessagingGateway.class)) { + BeanDefinition beanDefinition = this.registry.getBeanDefinition(beanName); + if (beanDefinition instanceof AnnotatedGenericBeanDefinition + || beanDefinition instanceof ScannedGenericBeanDefinition) { - AnnotationGatewayProxyFactoryBean gatewayProxyFactoryBean = - new AnnotationGatewayProxyFactoryBean<>(beanClass); - gatewayProxyFactoryBean.setApplicationContext(this.applicationContext); - gatewayProxyFactoryBean.setBeanFactory(this.applicationContext.getAutowireCapableBeanFactory()); - ClassLoader classLoader = this.applicationContext.getClassLoader(); - if (classLoader != null) { - gatewayProxyFactoryBean.setBeanClassLoader(classLoader); + AnnotationGatewayProxyFactoryBean gatewayProxyFactoryBean = + new AnnotationGatewayProxyFactoryBean<>(beanClass); + gatewayProxyFactoryBean.setApplicationContext(this.applicationContext); + gatewayProxyFactoryBean.setBeanFactory(this.applicationContext.getAutowireCapableBeanFactory()); + ClassLoader classLoader = this.applicationContext.getClassLoader(); + if (classLoader != null) { + gatewayProxyFactoryBean.setBeanClassLoader(classLoader); + } + gatewayProxyFactoryBean.setBeanName(beanName); + gatewayProxyFactoryBean.afterPropertiesSet(); + return gatewayProxyFactoryBean; } - gatewayProxyFactoryBean.setBeanName(beanName); - return gatewayProxyFactoryBean; } return null; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java index 32c5c19fc7..09e5fb0f14 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2022 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,10 +20,8 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -33,11 +31,12 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; @@ -67,15 +66,14 @@ import org.springframework.util.StringUtils; public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { - private final Map componentRegistrars = new HashMap<>(); + private final List defaultFilters = new ArrayList<>(); private ResourceLoader resourceLoader; private Environment environment; public IntegrationComponentScanRegistrar() { - this.componentRegistrars.put(new AnnotationTypeFilter(MessagingGateway.class, true), - new MessagingGatewayRegistrar()); + this.defaultFilters.add(new AnnotationTypeFilter(MessagingGateway.class, true)); } @Override @@ -90,84 +88,75 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - Map componentScan = - importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName()); + AnnotationAttributes componentScan = + AnnotationAttributes.fromMap( + importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName())); - Collection basePackages = getBasePackages(importingClassMetadata, registry); + Collection basePackages = getBasePackages(componentScan, registry); if (basePackages.isEmpty()) { basePackages = Collections.singleton(ClassUtils.getPackageName(importingClassMetadata.getClassName())); } - ClassPathScanningCandidateComponentProvider scanner = - new ClassPathScanningCandidateComponentProvider(false, this.environment) { + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false) { - @Override - protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { - return beanDefinition.getMetadata().isIndependent() - && !beanDefinition.getMetadata().isAnnotation(); - } + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return beanDefinition.getMetadata().isIndependent() + && !beanDefinition.getMetadata().isAnnotation(); + } - @Override - protected BeanDefinitionRegistry getRegistry() { - return registry; - } - - }; + }; filter(registry, componentScan, scanner); // NOSONAR - never null scanner.setResourceLoader(this.resourceLoader); + scanner.setEnvironment(this.environment); - for (String basePackage : basePackages) { - Set candidateComponents = scanner.findCandidateComponents(basePackage); - for (BeanDefinition candidateComponent : candidateComponents) { - if (candidateComponent instanceof AnnotatedBeanDefinition) { - for (ImportBeanDefinitionRegistrar registrar : this.componentRegistrars.values()) { - registrar.registerBeanDefinitions(((AnnotatedBeanDefinition) candidateComponent).getMetadata(), - registry); - } - } - } + BeanNameGenerator beanNameGenerator = IntegrationConfigUtils.annotationBeanNameGenerator(registry); + + Class generatorClass = componentScan.getClass("nameGenerator"); + if (!(BeanNameGenerator.class == generatorClass)) { + beanNameGenerator = BeanUtils.instantiateClass(generatorClass); } + + scanner.setBeanNameGenerator(beanNameGenerator); + scanner.scan(basePackages.toArray(String[]::new)); } - private void filter(BeanDefinitionRegistry registry, Map componentScan, + private void filter(BeanDefinitionRegistry registry, AnnotationAttributes componentScan, ClassPathScanningCandidateComponentProvider scanner) { - if ((boolean) componentScan.get("useDefaultFilters")) { // NOSONAR - never null - for (TypeFilter typeFilter : this.componentRegistrars.keySet()) { + if (componentScan.getBoolean("useDefaultFilters")) { // NOSONAR - never null + for (TypeFilter typeFilter : this.defaultFilters) { scanner.addIncludeFilter(typeFilter); } } - for (AnnotationAttributes filter : (AnnotationAttributes[]) componentScan.get("includeFilters")) { + for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter, registry)) { scanner.addIncludeFilter(typeFilter); } } - for (AnnotationAttributes filter : (AnnotationAttributes[]) componentScan.get("excludeFilters")) { + for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) { for (TypeFilter typeFilter : typeFiltersFor(filter, registry)) { scanner.addExcludeFilter(typeFilter); } } } - protected Collection getBasePackages(AnnotationMetadata importingClassMetadata, + protected Collection getBasePackages(AnnotationAttributes componentScan, @SuppressWarnings("unused") BeanDefinitionRegistry registry) { - Map componentScan = - importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName()); - Set basePackages = new HashSet<>(); - for (String pkg : (String[]) componentScan.get("value")) { // NOSONAR - never null + for (String pkg : componentScan.getStringArray("value")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } - for (Class clazz : (Class[]) componentScan.get("basePackageClasses")) { + for (Class clazz : componentScan.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } @@ -180,38 +169,33 @@ public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRe for (Class filterClass : filter.getClassArray("classes")) { switch (filterType) { - case ANNOTATION: + case ANNOTATION -> { Assert.isAssignable(Annotation.class, filterClass, "An error occurred while processing a @IntegrationComponentScan ANNOTATION type filter: "); @SuppressWarnings("unchecked") Class annotationType = (Class) filterClass; typeFilters.add(new AnnotationTypeFilter(annotationType)); - break; - case ASSIGNABLE_TYPE: - typeFilters.add(new AssignableTypeFilter(filterClass)); - break; - case CUSTOM: + } + case ASSIGNABLE_TYPE -> typeFilters.add(new AssignableTypeFilter(filterClass)); + case CUSTOM -> { Assert.isAssignable(TypeFilter.class, filterClass, "An error occurred while processing a @IntegrationComponentScan CUSTOM type filter: "); TypeFilter typeFilter = BeanUtils.instantiateClass(filterClass, TypeFilter.class); invokeAwareMethods(filter, this.environment, this.resourceLoader, registry); typeFilters.add(typeFilter); - break; - default: - throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType); + } + default -> throw new IllegalArgumentException("Filter type not supported with Class value: " + + filterType); } } for (String expression : filter.getStringArray("pattern")) { switch (filterType) { - case ASPECTJ: - typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader())); - break; - case REGEX: - typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); - break; - default: - throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType); + case ASPECTJ -> + typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader())); + case REGEX -> typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression))); + default -> throw new IllegalArgumentException("Filter type not supported with String pattern: " + + filterType); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationConfigUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationConfigUtils.java index 117ead890d..a7bce6c1ca 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationConfigUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationConfigUtils.java @@ -16,9 +16,13 @@ package org.springframework.integration.config; +import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; +import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.integration.channel.DirectChannel; /** @@ -64,6 +68,20 @@ public final class IntegrationConfigUtils { registry.registerBeanDefinition(channelName, new RootBeanDefinition(DirectChannel.class, DirectChannel::new)); } + public static BeanNameGenerator annotationBeanNameGenerator(BeanDefinitionRegistry registry) { + BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE; + if (registry instanceof SingletonBeanRegistry singletonBeanRegistry) { + BeanNameGenerator generator = + (BeanNameGenerator) singletonBeanRegistry.getSingleton( + AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); + if (generator != null) { + beanNameGenerator = generator; + } + } + + return beanNameGenerator; + } + private IntegrationConfigUtils() { } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java b/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java deleted file mode 100644 index 2ad7f7bee8..0000000000 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2014-2022 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 java.beans.Introspector; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.springframework.beans.factory.BeanDefinitionStoreException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.context.annotation.Primary; -import org.springframework.core.ResolvableType; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.expression.common.LiteralExpression; -import org.springframework.integration.annotation.AnnotationConstants; -import org.springframework.integration.annotation.MessagingGateway; -import org.springframework.integration.gateway.GatewayMethodMetadata; -import org.springframework.integration.gateway.GatewayProxyFactoryBean; -import org.springframework.integration.gateway.RequestReplyExchanger; -import org.springframework.integration.util.MessagingAnnotationUtils; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * The {@link ImportBeanDefinitionRegistrar} to parse {@link MessagingGateway} and its {@code service-interface} - * and to register {@link BeanDefinition} {@link GatewayProxyFactoryBean}. - * - * @author Artem Bilan - * @author Gary Russell - * @author Andy Wilksinson - * - * @since 4.0 - */ -public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar { - - private static final String PROXY_DEFAULT_METHODS_ATTR = "proxyDefaultMethods"; - - private static final String PRIMARY_ATTR = "primary"; - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - if (importingClassMetadata != null && importingClassMetadata.isAnnotated(MessagingGateway.class.getName())) { - Assert.isTrue(importingClassMetadata.isInterface(), - "@MessagingGateway can only be specified on an interface"); - List> valuesHierarchy = captureMetaAnnotationValues(importingClassMetadata); - Map annotationAttributes = - importingClassMetadata.getAnnotationAttributes(MessagingGateway.class.getName()); - replaceEmptyOverrides(valuesHierarchy, annotationAttributes); // NOSONAR never null - annotationAttributes.put("serviceInterface", importingClassMetadata.getClassName()); - annotationAttributes.put(PROXY_DEFAULT_METHODS_ATTR, - "" + annotationAttributes.remove(PROXY_DEFAULT_METHODS_ATTR)); - if (importingClassMetadata.isAnnotated(Primary.class.getName())) { - annotationAttributes.put(PRIMARY_ATTR, true); - } - BeanDefinitionReaderUtils.registerBeanDefinition(parse(annotationAttributes, registry), registry); - } - } - - public BeanDefinitionHolder parse(Map gatewayAttributes, BeanDefinitionRegistry registry) { // NOSONAR complexity - String defaultPayloadExpression = (String) gatewayAttributes.get("defaultPayloadExpression"); - - @SuppressWarnings("unchecked") - Map[] defaultHeaders = (Map[]) gatewayAttributes.get("defaultHeaders"); - - String defaultRequestChannel = (String) gatewayAttributes.get("defaultRequestChannel"); - String defaultReplyChannel = (String) gatewayAttributes.get("defaultReplyChannel"); - String errorChannel = (String) gatewayAttributes.get("errorChannel"); - String asyncExecutor = (String) gatewayAttributes.get("asyncExecutor"); - String mapper = (String) gatewayAttributes.get("mapper"); - String proxyDefaultMethods = (String) gatewayAttributes.get(PROXY_DEFAULT_METHODS_ATTR); - - boolean hasMapper = StringUtils.hasText(mapper); - boolean hasDefaultPayloadExpression = StringUtils.hasText(defaultPayloadExpression); - Assert.state(!hasMapper || !hasDefaultPayloadExpression, - "'defaultPayloadExpression' is not allowed when a 'mapper' is provided"); - - boolean hasDefaultHeaders = !ObjectUtils.isEmpty(defaultHeaders); - Assert.state(!hasMapper || !hasDefaultHeaders, - "'defaultHeaders' are not allowed when a 'mapper' is provided"); - - ConfigurableBeanFactory beanFactory = obtainBeanFactory(registry); - Class serviceInterface = getServiceInterface((String) gatewayAttributes.get("serviceInterface"), beanFactory); - - BeanDefinitionBuilder gatewayProxyBuilder = - BeanDefinitionBuilder.rootBeanDefinition(GatewayProxyFactoryBean.class, - () -> new GatewayProxyFactoryBean<>(serviceInterface)); - - if (hasDefaultHeaders || hasDefaultPayloadExpression) { - BeanDefinitionBuilder methodMetadataBuilder = - BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodMetadata.class, GatewayMethodMetadata::new); - - if (hasDefaultPayloadExpression) { - methodMetadataBuilder.addPropertyValue("payloadExpression", - BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class) - .addConstructorArgValue(defaultPayloadExpression) - .getBeanDefinition()); - } - - if (hasDefaultHeaders) { - Map headerExpressions = new ManagedMap<>(); - for (Map header : defaultHeaders) { - String headerValue = (String) header.get("value"); - String headerExpression = (String) header.get("expression"); - boolean hasValue = StringUtils.hasText(headerValue); - - if (hasValue == StringUtils.hasText(headerExpression)) { - throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' " + - "is required on a gateway's header."); - } - - BeanDefinition expressionDef = - new RootBeanDefinition(hasValue ? LiteralExpression.class : ExpressionFactoryBean.class); - expressionDef.getConstructorArgumentValues() - .addGenericArgumentValue(hasValue ? headerValue : headerExpression); - - headerExpressions.put((String) header.get("name"), expressionDef); - } - methodMetadataBuilder.addPropertyValue("headerExpressions", headerExpressions); - } - - gatewayProxyBuilder.addPropertyValue("globalMethodMetadata", methodMetadataBuilder.getBeanDefinition()); - } - - - if (StringUtils.hasText(defaultRequestChannel)) { - gatewayProxyBuilder.addPropertyValue("defaultRequestChannelName", defaultRequestChannel); - } - if (StringUtils.hasText(defaultReplyChannel)) { - gatewayProxyBuilder.addPropertyValue("defaultReplyChannelName", defaultReplyChannel); - } - if (StringUtils.hasText(errorChannel)) { - gatewayProxyBuilder.addPropertyValue("errorChannelName", errorChannel); - } - if (asyncExecutor == null || AnnotationConstants.NULL.equals(asyncExecutor)) { - gatewayProxyBuilder.addPropertyValue("asyncExecutor", null); - } - else if (StringUtils.hasText(asyncExecutor)) { - gatewayProxyBuilder.addPropertyReference("asyncExecutor", asyncExecutor); - } - if (StringUtils.hasText(mapper)) { - gatewayProxyBuilder.addPropertyReference("mapper", mapper); - } - if (StringUtils.hasText(proxyDefaultMethods)) { - gatewayProxyBuilder.addPropertyValue(PROXY_DEFAULT_METHODS_ATTR, proxyDefaultMethods); - } - - gatewayProxyBuilder.addPropertyValue("defaultRequestTimeoutExpressionString", - gatewayAttributes.get("defaultRequestTimeout")); - gatewayProxyBuilder.addPropertyValue("defaultReplyTimeoutExpressionString", - gatewayAttributes.get("defaultReplyTimeout")); - gatewayProxyBuilder.addPropertyValue("methodMetadataMap", gatewayAttributes.get("methods")); - - - String id = (String) gatewayAttributes.get("name"); - if (!StringUtils.hasText(id)) { - String serviceInterfaceName = serviceInterface.getName(); - id = Introspector.decapitalize(serviceInterfaceName.substring(serviceInterfaceName.lastIndexOf('.') + 1)); - } - - gatewayProxyBuilder.addConstructorArgValue(serviceInterface); - gatewayProxyBuilder.setPrimary(gatewayAttributes.containsKey(PRIMARY_ATTR)); - - RootBeanDefinition beanDefinition = (RootBeanDefinition) gatewayProxyBuilder.getBeanDefinition(); - beanDefinition.setTargetType( - ResolvableType.forClassWithGenerics(GatewayProxyFactoryBean.class, serviceInterface)); - return new BeanDefinitionHolder(beanDefinition, id); - } - - /** - * TODO until SPR-11710 will be resolved. - * Captures the meta-annotation attribute values, in order. - * @param importingClassMetadata The importing class metadata - * @return The captured values. - */ - private static List> captureMetaAnnotationValues( - AnnotationMetadata importingClassMetadata) { - - Set directAnnotations = importingClassMetadata.getAnnotationTypes(); - List> valuesHierarchy = new ArrayList<>(); - // Need to grab the values now; see SPR-11710 - for (String ann : directAnnotations) { - Set chain = importingClassMetadata.getMetaAnnotationTypes(ann); - if (chain.contains(MessagingGateway.class.getName())) { - for (String meta : chain) { - MultiValueMap attributes = importingClassMetadata.getAllAnnotationAttributes(meta); - if (attributes != null) { - valuesHierarchy.add(attributes); - } - } - } - } - return valuesHierarchy; - } - - /** - * TODO until SPR-11709 will be resolved. - * For any empty values, traverses back up the meta-annotation hierarchy to - * see if a value has been overridden to empty, and replaces the first such value found. - * @param valuesHierarchy The values hierarchy in order. - * @param annotationAttributes The current attribute values. - */ - private static void replaceEmptyOverrides(List> valuesHierarchy, - Map annotationAttributes) { - - for (Entry entry : annotationAttributes.entrySet()) { - Object value = entry.getValue(); - if (!MessagingAnnotationUtils.hasValue(value)) { - // see if we overrode a value that was higher in the annotation chain - for (MultiValueMap metaAttributesMap : valuesHierarchy) { - Object newValue = metaAttributesMap.getFirst(entry.getKey()); - if (MessagingAnnotationUtils.hasValue(newValue)) { - annotationAttributes.put(entry.getKey(), newValue); - break; - } - } - } - } - } - - private static ConfigurableBeanFactory obtainBeanFactory(BeanDefinitionRegistry registry) { - if (registry instanceof ConfigurableBeanFactory) { - return (ConfigurableBeanFactory) registry; - } - else if (registry instanceof ConfigurableApplicationContext) { - return ((ConfigurableApplicationContext) registry).getBeanFactory(); - } - throw new IllegalArgumentException("The provided 'BeanDefinitionRegistry' must be an instance " + - "of 'ConfigurableBeanFactory' or 'ConfigurableApplicationContext', but given is: " - + registry.getClass()); - } - - private static Class getServiceInterface(String serviceInterface, ConfigurableBeanFactory beanFactory) { - String actualServiceInterface = beanFactory.resolveEmbeddedValue(serviceInterface); - if (!StringUtils.hasText(actualServiceInterface)) { - return RequestReplyExchanger.class; - } - try { - return ClassUtils.forName(actualServiceInterface, beanFactory.getBeanClassLoader()); - } - catch (ClassNotFoundException ex) { - throw new BeanDefinitionStoreException("Cannot parse class for service interface", ex); - } - } - -} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java index 486e64e542..4dc3c48fe8 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -23,19 +23,33 @@ import java.util.Map; import org.w3c.dom.Element; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.annotation.AnnotationConstants; import org.springframework.integration.config.ExpressionFactoryBean; -import org.springframework.integration.config.MessagingGatewayRegistrar; +import org.springframework.integration.config.IntegrationConfigUtils; import org.springframework.integration.gateway.GatewayMethodMetadata; +import org.springframework.integration.gateway.GatewayProxyFactoryBean; +import org.springframework.integration.gateway.RequestReplyExchanger; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -49,7 +63,7 @@ import org.springframework.util.xml.DomUtils; */ public class GatewayParser implements BeanDefinitionParser { - private final MessagingGatewayRegistrar registrar = new MessagingGatewayRegistrar(); + private static final String PROXY_DEFAULT_METHODS_ATTR = "proxyDefaultMethods"; @Override public BeanDefinition parse(final Element element, ParserContext parserContext) { @@ -88,7 +102,7 @@ public class GatewayParser implements BeanDefinitionParser { gatewayAttributes.put("proxyDefaultMethods", element.getAttribute("proxy-default-methods")); - BeanDefinitionHolder gatewayHolder = this.registrar.parse(gatewayAttributes, parserContext.getRegistry()); + BeanDefinitionHolder gatewayHolder = buildBeanDefinition(gatewayAttributes, parserContext); if (isNested) { return gatewayHolder.getBeanDefinition(); } @@ -98,7 +112,7 @@ public class GatewayParser implements BeanDefinitionParser { } } - private void headers(Element element, Map gatewayAttributes) { + private static void headers(Element element, Map gatewayAttributes) { List headerElements = DomUtils.getChildElementsByTagName(element, "default-header"); if (!CollectionUtils.isEmpty(headerElements)) { List> headers = new ArrayList<>(headerElements.size()); @@ -114,8 +128,9 @@ public class GatewayParser implements BeanDefinitionParser { } } - private void methods(final Element element, ParserContext parserContext, + private static void methods(Element element, ParserContext parserContext, final Map gatewayAttributes) { + List methodElements = DomUtils.getChildElementsByTagName(element, "method"); if (!CollectionUtils.isEmpty(methodElements)) { Map methodMetadataMap = new ManagedMap<>(); @@ -163,4 +178,146 @@ public class GatewayParser implements BeanDefinitionParser { } } + + private static BeanDefinitionHolder buildBeanDefinition(Map gatewayAttributes, // NOSONAR complexity + ParserContext parserContext) { + + BeanDefinitionRegistry registry = parserContext.getRegistry(); + String defaultPayloadExpression = (String) gatewayAttributes.get("defaultPayloadExpression"); + + @SuppressWarnings("unchecked") + Map[] defaultHeaders = (Map[]) gatewayAttributes.get("defaultHeaders"); + + String defaultRequestChannel = (String) gatewayAttributes.get("defaultRequestChannel"); + String defaultReplyChannel = (String) gatewayAttributes.get("defaultReplyChannel"); + String errorChannel = (String) gatewayAttributes.get("errorChannel"); + String asyncExecutor = (String) gatewayAttributes.get("asyncExecutor"); + String mapper = (String) gatewayAttributes.get("mapper"); + String proxyDefaultMethods = (String) gatewayAttributes.get(PROXY_DEFAULT_METHODS_ATTR); + + boolean hasMapper = StringUtils.hasText(mapper); + boolean hasDefaultPayloadExpression = StringUtils.hasText(defaultPayloadExpression); + Assert.state(!hasMapper || !hasDefaultPayloadExpression, + "'defaultPayloadExpression' is not allowed when a 'mapper' is provided"); + + boolean hasDefaultHeaders = !ObjectUtils.isEmpty(defaultHeaders); + Assert.state(!hasMapper || !hasDefaultHeaders, + "'defaultHeaders' are not allowed when a 'mapper' is provided"); + + ConfigurableBeanFactory beanFactory = obtainBeanFactory(registry); + Class serviceInterface = getServiceInterface((String) gatewayAttributes.get("serviceInterface"), beanFactory); + + BeanDefinitionBuilder gatewayProxyBuilder = + BeanDefinitionBuilder.rootBeanDefinition(GatewayProxyFactoryBean.class) + .addConstructorArgValue(serviceInterface); + + if (hasDefaultHeaders || hasDefaultPayloadExpression) { + BeanDefinition methodMetadata = getMethodMetadataBeanDefinition(defaultPayloadExpression, defaultHeaders); + + gatewayProxyBuilder.addPropertyValue("globalMethodMetadata", methodMetadata); + } + + if (StringUtils.hasText(defaultRequestChannel)) { + gatewayProxyBuilder.addPropertyValue("defaultRequestChannelName", defaultRequestChannel); + } + if (StringUtils.hasText(defaultReplyChannel)) { + gatewayProxyBuilder.addPropertyValue("defaultReplyChannelName", defaultReplyChannel); + } + if (StringUtils.hasText(errorChannel)) { + gatewayProxyBuilder.addPropertyValue("errorChannelName", errorChannel); + } + if (asyncExecutor == null || AnnotationConstants.NULL.equals(asyncExecutor)) { + gatewayProxyBuilder.addPropertyValue("asyncExecutor", null); + } + else if (StringUtils.hasText(asyncExecutor)) { + gatewayProxyBuilder.addPropertyReference("asyncExecutor", asyncExecutor); + } + if (StringUtils.hasText(mapper)) { + gatewayProxyBuilder.addPropertyReference("mapper", mapper); + } + if (StringUtils.hasText(proxyDefaultMethods)) { + gatewayProxyBuilder.addPropertyValue(PROXY_DEFAULT_METHODS_ATTR, proxyDefaultMethods); + } + + gatewayProxyBuilder.addPropertyValue("defaultRequestTimeoutExpressionString", + gatewayAttributes.get("defaultRequestTimeout")); + gatewayProxyBuilder.addPropertyValue("defaultReplyTimeoutExpressionString", + gatewayAttributes.get("defaultReplyTimeout")); + gatewayProxyBuilder.addPropertyValue("methodMetadataMap", gatewayAttributes.get("methods")); + + String id = (String) gatewayAttributes.get("name"); + if (!StringUtils.hasText(id)) { + BeanNameGenerator beanNameGenerator = + IntegrationConfigUtils.annotationBeanNameGenerator(registry); + id = beanNameGenerator.generateBeanName(new AnnotatedGenericBeanDefinition(serviceInterface), registry); + } + + RootBeanDefinition beanDefinition = (RootBeanDefinition) gatewayProxyBuilder.getBeanDefinition(); + beanDefinition.setTargetType( + ResolvableType.forClassWithGenerics(GatewayProxyFactoryBean.class, serviceInterface)); + return new BeanDefinitionHolder(beanDefinition, id); + } + + private static BeanDefinition getMethodMetadataBeanDefinition(String defaultPayloadExpression, + Map[] defaultHeaders) { + + BeanDefinitionBuilder methodMetadataBuilder = + BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodMetadata.class, GatewayMethodMetadata::new); + + if (StringUtils.hasText(defaultPayloadExpression)) { + methodMetadataBuilder.addPropertyValue("payloadExpression", + BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class) + .addConstructorArgValue(defaultPayloadExpression) + .getBeanDefinition()); + } + + if (!ObjectUtils.isEmpty(defaultHeaders)) { + Map headerExpressions = new ManagedMap<>(); + for (Map header : defaultHeaders) { + String headerValue = (String) header.get("value"); + String headerExpression = (String) header.get("expression"); + boolean hasValue = StringUtils.hasText(headerValue); + + if (hasValue == StringUtils.hasText(headerExpression)) { + throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' " + + "is required on a gateway's header."); + } + + BeanDefinition expressionDef = + new RootBeanDefinition(hasValue ? LiteralExpression.class : ExpressionFactoryBean.class); + expressionDef.getConstructorArgumentValues() + .addGenericArgumentValue(hasValue ? headerValue : headerExpression); + + headerExpressions.put((String) header.get("name"), expressionDef); + } + methodMetadataBuilder.addPropertyValue("headerExpressions", headerExpressions); + } + return methodMetadataBuilder.getBeanDefinition(); + } + + private static ConfigurableBeanFactory obtainBeanFactory(BeanDefinitionRegistry registry) { + if (registry instanceof ConfigurableBeanFactory) { + return (ConfigurableBeanFactory) registry; + } + else if (registry instanceof ConfigurableApplicationContext) { + return ((ConfigurableApplicationContext) registry).getBeanFactory(); + } + throw new IllegalArgumentException("The provided 'BeanDefinitionRegistry' must be an instance " + + "of 'ConfigurableBeanFactory' or 'ConfigurableApplicationContext', but given is: " + + registry.getClass()); + } + + private static Class getServiceInterface(String serviceInterface, ConfigurableBeanFactory beanFactory) { + String actualServiceInterface = beanFactory.resolveEmbeddedValue(serviceInterface); + if (!StringUtils.hasText(actualServiceInterface)) { + return RequestReplyExchanger.class; + } + try { + return ClassUtils.forName(actualServiceInterface, beanFactory.getBeanClassLoader()); + } + catch (ClassNotFoundException ex) { + throw new BeanDefinitionStoreException("Cannot parse class for service interface", ex); + } + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java index 7ee6e06763..97669aac0e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/AnnotationGatewayProxyFactoryBean.java @@ -16,21 +16,29 @@ package org.springframework.integration.gateway; +import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import java.util.stream.Collectors; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.expression.StandardBeanExpressionResolver; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.JavaUtils; import org.springframework.integration.annotation.AnnotationConstants; +import org.springframework.integration.annotation.GatewayHeader; import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.util.MessagingAnnotationUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -52,32 +60,55 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBea private final AnnotationAttributes gatewayAttributes; + private BeanExpressionResolver resolver = new StandardBeanExpressionResolver(); + + private BeanExpressionContext expressionContext; + public AnnotationGatewayProxyFactoryBean(Class serviceInterface) { super(serviceInterface); + this.gatewayAttributes = mergeAnnotationAttributes(serviceInterface); + } + private static AnnotationAttributes mergeAnnotationAttributes(Class serviceInterface) { AnnotationAttributes annotationAttributes = - AnnotatedElementUtils.getMergedAnnotationAttributes(serviceInterface, - MessagingGateway.class.getName(), false, true); - if (annotationAttributes == null) { - annotationAttributes = AnnotationUtils.getAnnotationAttributes( - AnnotationUtils.synthesizeAnnotation(MessagingGateway.class), false, true); + AnnotationUtils.getAnnotationAttributes(null, + AnnotationUtils.synthesizeAnnotation(MessagingGateway.class)); + + if (AnnotatedElementUtils.isAnnotated(serviceInterface, MessagingGateway.class)) { + Annotation annotation = + MergedAnnotations.from(serviceInterface) + .get(MessagingGateway.class) + .getRoot() + .synthesize(); + + List annotationChain = + MessagingAnnotationUtils.getAnnotationChain(annotation, MessagingGateway.class); + + for (Map.Entry attribute : annotationAttributes.entrySet()) { + String key = attribute.getKey(); + Object value = MessagingAnnotationUtils.resolveAttribute(annotationChain, key, Object.class); + if (value != null) { + attribute.setValue(value); + } + } } - this.gatewayAttributes = annotationAttributes; - - String id = annotationAttributes.getString("name"); - if (StringUtils.hasText(id)) { - setBeanName(id); - } + return annotationAttributes; } @Override protected void onInit() { + if (getBeanFactory() instanceof ConfigurableBeanFactory beanFactory) { + this.resolver = beanFactory.getBeanExpressionResolver(); + this.expressionContext = new BeanExpressionContext(beanFactory, null); + } + populateGatewayMethodMetadataIfAny(); String defaultRequestTimeout = resolveAttribute("defaultRequestTimeout"); String defaultReplyTimeout = resolveAttribute("defaultReplyTimeout"); + JavaUtils.INSTANCE .acceptIfCondition(getDefaultRequestChannel() == null && getDefaultRequestChannelName() == null, resolveAttribute("defaultRequestChannel"), @@ -89,11 +120,11 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBea resolveAttribute("errorChannel"), this::setErrorChannelName) .acceptIfCondition(getDefaultRequestTimeout() == null && StringUtils.hasText(defaultRequestTimeout), - defaultRequestTimeout, - value -> setDefaultRequestTimeout(Long.parseLong(value))) + evaluateExpression(defaultRequestTimeout, Long.class), + this::setDefaultRequestTimeout) .acceptIfCondition(getDefaultReplyTimeout() == null && StringUtils.hasText(defaultReplyTimeout), - defaultReplyTimeout, - value -> setDefaultReplyTimeout(Long.parseLong(value))); + evaluateExpression(defaultReplyTimeout, Long.class), + this::setDefaultReplyTimeout); populateAsyncExecutorIfAny(); @@ -109,12 +140,11 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBea return; } - ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) getBeanFactory(); + ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) getBeanFactory(); String defaultPayloadExpression = resolveAttribute("defaultPayloadExpression"); - @SuppressWarnings("unchecked") - Map[] defaultHeaders = (Map[]) this.gatewayAttributes.get("defaultHeaders"); + GatewayHeader[] defaultHeaders = (GatewayHeader[]) this.gatewayAttributes.get("defaultHeaders"); String mapper = resolveAttribute("mapper"); @@ -141,14 +171,14 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBea Map headerExpressions = Arrays.stream(defaultHeaders) .collect(Collectors.toMap( - header -> beanFactory.resolveEmbeddedValue((String) header.get("name")), + header -> beanFactory.resolveEmbeddedValue(header.name()), header -> { String headerValue = - beanFactory.resolveEmbeddedValue((String) header.get("value")); + beanFactory.resolveEmbeddedValue(header.value()); boolean hasValue = StringUtils.hasText(headerValue); String headerExpression = - beanFactory.resolveEmbeddedValue((String) header.get("expression")); + beanFactory.resolveEmbeddedValue(header.expression()); Assert.state(!(hasValue == StringUtils.hasText(headerExpression)), "exactly one of 'value' or 'expression' is required on a gateway's " + @@ -180,8 +210,19 @@ public class AnnotationGatewayProxyFactoryBean extends GatewayProxyFactoryBea @Nullable private String resolveAttribute(String attributeName) { - ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) getBeanFactory(); + ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) getBeanFactory(); return beanFactory.resolveEmbeddedValue(this.gatewayAttributes.getString(attributeName)); } + @Nullable + private V evaluateExpression(@Nullable String value, Class targetClass) { + if (StringUtils.hasText(value)) { + Object result = this.resolver.evaluate(value, this.expressionContext); + return getConversionService().convert(result, targetClass); + } + else { + return null; + } + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java index ec83487da6..86077f847e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java @@ -64,6 +64,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.expression.EnvironmentAccessor; import org.springframework.context.expression.MapAccessor; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogAccessor; import org.springframework.core.serializer.support.SerializingConverter; @@ -488,6 +489,10 @@ public class EnableIntegrationTests { @Test public void testMessagingGateway() throws InterruptedException { + String gatewayBeanName = AnnotatedElementUtils.findMergedAnnotation(TestGateway.class, Component.class).value(); + assertThat(gatewayBeanName).isEqualTo("namedTestGateway"); + assertThat(this.testGateway).isSameAs(this.context.getBean(gatewayBeanName)); + String payload = "bar"; String result = this.testGateway.echo(payload); assertThat(result.substring(0, payload.length())).isEqualTo(payload.toUpperCase()); @@ -1464,7 +1469,7 @@ public class EnableIntegrationTests { } - @TestMessagingGateway + @TestMessagingGateway("namedTestGateway") public interface TestGateway { @Gateway(headers = @GatewayHeader(name = "calledMethod", expression = "method.name")) @@ -1494,6 +1499,9 @@ public class EnableIntegrationTests { defaultHeaders = @GatewayHeader(name = "foo", value = "FOO")) public @interface TestMessagingGateway { + @AliasFor(annotation = MessagingGateway.class, attribute = "value") + String value() default ""; + @AliasFor(annotation = MessagingGateway.class, attribute = "defaultRequestChannel") String defaultRequestChannel() default ""; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/enablecomponentscan/EnableComponentScanTests.java b/spring-integration-core/src/test/java/org/springframework/integration/enablecomponentscan/EnableComponentScanTests.java index abd1513550..3f4f7202db 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/enablecomponentscan/EnableComponentScanTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/enablecomponentscan/EnableComponentScanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2022 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,21 +21,21 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.Collection; import java.util.Collections; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.integration.annotation.IntegrationComponentScan; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.IntegrationComponentScanRegistrar; import org.springframework.integration.dsl.flows.IntegrationFlowTests; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.ClassUtils; /** @@ -44,7 +44,7 @@ import org.springframework.util.ClassUtils; * * @since 4.0 */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig @DirtiesContext public class EnableComponentScanTests { @@ -68,13 +68,15 @@ public class EnableComponentScanTests { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + super.registerBeanDefinitions( AnnotationMetadata.introspect(IntegrationComponentScanConfiguration.class), registry); } @Override - protected Collection getBasePackages(AnnotationMetadata importingClassMetadata, + protected Collection getBasePackages(AnnotationAttributes importingClassMetadata, BeanDefinitionRegistry registry) { + return Collections.singleton(ClassUtils.getPackageName(IntegrationFlowTests.ControlBusGateway.class)); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java index cfc0741acd..769fb47d87 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInterfaceTests.java @@ -45,8 +45,10 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Condition; @@ -94,6 +96,7 @@ import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.ClassUtils; /** * @author Oleg Zhurakousky @@ -121,11 +124,11 @@ public class GatewayInterfaceTests { private NoExecGateway noExecGateway; @Autowired - @Qualifier("&gatewayInterfaceTests$ExecGateway") + @Qualifier("&execGateway") private GatewayProxyFactoryBean execGatewayFB; @Autowired - @Qualifier("&gatewayInterfaceTests$NoExecGateway") + @Qualifier("&noExecutorGateway") private GatewayProxyFactoryBean noExecGatewayFB; @Autowired @@ -642,8 +645,10 @@ public class GatewayInterfaceTests { @Configuration @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AutoCreateChannelService.class)) - @IntegrationComponentScan(useDefaultFilters = false, - includeFilters = @ComponentScan.Filter(TestMessagingGateway.class)) + @IntegrationComponentScan( + useDefaultFilters = false, + includeFilters = @ComponentScan.Filter(TestMessagingGateway.class), + nameGenerator = CustomBeanNameGenerator.class) @EnableIntegration @Import(ImportedGateway.class) public static class TestConfig { @@ -754,7 +759,7 @@ public class GatewayInterfaceTests { } - @MessagingGateway(asyncExecutor = AnnotationConstants.NULL) + @MessagingGateway(name = "noExecutorGateway", asyncExecutor = AnnotationConstants.NULL) @TestMessagingGateway public interface NoExecGateway { @@ -836,4 +841,20 @@ public class GatewayInterfaceTests { } + public static class CustomBeanNameGenerator extends AnnotationBeanNameGenerator { + + @Override + protected String buildDefaultBeanName(BeanDefinition definition) { + try { + Class beanClass = ClassUtils.forName(definition.getBeanClassName(), + ClassUtils.getDefaultClassLoader()); + return ClassUtils.getShortNameAsProperty(beanClass); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } + + } + } diff --git a/src/reference/asciidoc/gateway.adoc b/src/reference/asciidoc/gateway.adoc index 24e0ba515e..19f87722d0 100644 --- a/src/reference/asciidoc/gateway.adoc +++ b/src/reference/asciidoc/gateway.adoc @@ -322,6 +322,10 @@ Starting with version 6.0, an interface with the `@MessagingGateway` can also be Starting with version 6.0, `@MessagingGateway` interfaces can be used in the standard Spring `@Import` configuration. This may be used as an alternative to the `@IntegrationComponentScan` or manual `AnnotationGatewayProxyFactoryBean` bean definitions. +The `@MessagingGateway` is meta-annotated with a `@MessageEndpoint` since version `6.0` and the `name()` attribute is, essentially, aliased to the `@Compnent.value()`. +This way the bean names generating strategy for gateway proxies is realigned with the standard Spring annotation configuration for scanned and imported components. +The default `AnnotationBeanNameGenerator` can be overridden globally via an `AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR` or as a `@IntegrationComponentScan.nameGenerator()` attribute. + NOTE: If you have no XML configuration, the `@EnableIntegration` annotation is required on at least one `@Configuration` class. See <<./overview.adoc#configuration-enable-integration,Configuration and `@EnableIntegration`>> for more information. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index e4beefc35d..27feca6a8b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -114,7 +114,10 @@ The Messaging Gateway interface method can now return `Future` and `Mono> for more information.