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
This commit is contained in:
Artem Bilan
2022-11-01 10:28:07 -04:00
committed by GitHub
parent 511cb7619b
commit 39bb09012a
14 changed files with 394 additions and 398 deletions

View File

@@ -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.
* <p>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<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
}

View File

@@ -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 "";
}

View File

@@ -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 <gateway/>}) 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.
* <p>
* 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

View File

@@ -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;
}

View File

@@ -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<TypeFilter, ImportBeanDefinitionRegistrar> componentRegistrars = new HashMap<>();
private final List<TypeFilter> 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<String, Object> componentScan =
importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName());
AnnotationAttributes componentScan =
AnnotationAttributes.fromMap(
importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName()));
Collection<String> basePackages = getBasePackages(importingClassMetadata, registry);
Collection<String> 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<BeanDefinition> 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<? extends BeanNameGenerator> 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<String, Object> 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<String> getBasePackages(AnnotationMetadata importingClassMetadata,
protected Collection<String> getBasePackages(AnnotationAttributes componentScan,
@SuppressWarnings("unused") BeanDefinitionRegistry registry) {
Map<String, Object> componentScan =
importingClassMetadata.getAnnotationAttributes(IntegrationComponentScan.class.getName());
Set<String> 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<Annotation> annotationType = (Class<Annotation>) 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);
}
}

View File

@@ -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() {
}

View File

@@ -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<MultiValueMap<String, Object>> valuesHierarchy = captureMetaAnnotationValues(importingClassMetadata);
Map<String, Object> 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<String, Object> gatewayAttributes, BeanDefinitionRegistry registry) { // NOSONAR complexity
String defaultPayloadExpression = (String) gatewayAttributes.get("defaultPayloadExpression");
@SuppressWarnings("unchecked")
Map<String, Object>[] defaultHeaders = (Map<String, Object>[]) 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<String, Object> headerExpressions = new ManagedMap<>();
for (Map<String, Object> 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<MultiValueMap<String, Object>> captureMetaAnnotationValues(
AnnotationMetadata importingClassMetadata) {
Set<String> directAnnotations = importingClassMetadata.getAnnotationTypes();
List<MultiValueMap<String, Object>> valuesHierarchy = new ArrayList<>();
// Need to grab the values now; see SPR-11710
for (String ann : directAnnotations) {
Set<String> chain = importingClassMetadata.getMetaAnnotationTypes(ann);
if (chain.contains(MessagingGateway.class.getName())) {
for (String meta : chain) {
MultiValueMap<String, Object> 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<MultiValueMap<String, Object>> valuesHierarchy,
Map<String, Object> annotationAttributes) {
for (Entry<String, Object> 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<String, Object> 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);
}
}
}

View File

@@ -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<String, Object> gatewayAttributes) {
private static void headers(Element element, Map<String, Object> gatewayAttributes) {
List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "default-header");
if (!CollectionUtils.isEmpty(headerElements)) {
List<Map<String, Object>> 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<String, Object> gatewayAttributes) {
List<Element> methodElements = DomUtils.getChildElementsByTagName(element, "method");
if (!CollectionUtils.isEmpty(methodElements)) {
Map<String, BeanDefinition> methodMetadataMap = new ManagedMap<>();
@@ -163,4 +178,146 @@ public class GatewayParser implements BeanDefinitionParser {
}
}
private static BeanDefinitionHolder buildBeanDefinition(Map<String, Object> gatewayAttributes, // NOSONAR complexity
ParserContext parserContext) {
BeanDefinitionRegistry registry = parserContext.getRegistry();
String defaultPayloadExpression = (String) gatewayAttributes.get("defaultPayloadExpression");
@SuppressWarnings("unchecked")
Map<String, Object>[] defaultHeaders = (Map<String, Object>[]) 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<String, Object>[] 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<String, Object> headerExpressions = new ManagedMap<>();
for (Map<String, Object> 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);
}
}
}

View File

@@ -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<T> extends GatewayProxyFactoryBea
private final AnnotationAttributes gatewayAttributes;
private BeanExpressionResolver resolver = new StandardBeanExpressionResolver();
private BeanExpressionContext expressionContext;
public AnnotationGatewayProxyFactoryBean(Class<T> 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<Annotation> annotationChain =
MessagingAnnotationUtils.getAnnotationChain(annotation, MessagingGateway.class);
for (Map.Entry<String, Object> 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<T> 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<T> extends GatewayProxyFactoryBea
return;
}
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) getBeanFactory();
ConfigurableBeanFactory beanFactory = (ConfigurableBeanFactory) getBeanFactory();
String defaultPayloadExpression = resolveAttribute("defaultPayloadExpression");
@SuppressWarnings("unchecked")
Map<String, Object>[] defaultHeaders = (Map<String, Object>[]) this.gatewayAttributes.get("defaultHeaders");
GatewayHeader[] defaultHeaders = (GatewayHeader[]) this.gatewayAttributes.get("defaultHeaders");
String mapper = resolveAttribute("mapper");
@@ -141,14 +171,14 @@ public class AnnotationGatewayProxyFactoryBean<T> extends GatewayProxyFactoryBea
Map<String, Expression> 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<T> 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> V evaluateExpression(@Nullable String value, Class<V> targetClass) {
if (StringUtils.hasText(value)) {
Object result = this.resolver.evaluate(value, this.expressionContext);
return getConversionService().convert(result, targetClass);
}
else {
return null;
}
}
}

View File

@@ -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 "";

View File

@@ -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<String> getBasePackages(AnnotationMetadata importingClassMetadata,
protected Collection<String> getBasePackages(AnnotationAttributes importingClassMetadata,
BeanDefinitionRegistry registry) {
return Collections.singleton(ClassUtils.getPackageName(IntegrationFlowTests.ControlBusGateway.class));
}

View File

@@ -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);
}
}
}
}

View File

@@ -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.

View File

@@ -114,7 +114,10 @@ The Messaging Gateway interface method can now return `Future<Void>` and `Mono<V
Alongside with a `@MessagingGateway` annotation the interface can also be marked with a `@Primary`.
`@MessagingGateway` interfaces can now be `@Import`ed into a configuration.
`@MessagingGateway` interfaces can now be use as an `@Import` resources for configuration.
The default naming strategy for gateway proxy beans can be customized via `@IntegrationComponentScan.nameGenerator()` attribute.
If `AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR` bean is present, it is consulted otherwise before falling back to the `AnnotationBeanNameGenerator`.
See <<./gateway.adoc#gateway, Messaging Gateway>> for more information.