Functional gateway bean definitions

* Rework `MessagingGatewayRegistrar` to parse messaging gateway annotation
an `<gateway>` XML using a supplier variant for bean definition.
Such a feature is required by Spring Native - otherwise we would need to
register reflection info for to many internal Spring Integration classes
* Such a change should benefit from regular JDK perspective, too -
we don't do reflection for this kind of bean registrations
This commit is contained in:
Artem Bilan
2021-07-02 14:59:40 -04:00
committed by Gary Russell
parent 1ac163330e
commit db4f120dac
3 changed files with 223 additions and 115 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -18,30 +18,39 @@ package org.springframework.integration.config;
import java.beans.Introspector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.FactoryBean;
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.config.EmbeddedValueResolver;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
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.core.type.AnnotationMetadata;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
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.MethodArgsMessageMapper;
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;
@@ -58,6 +67,8 @@ import org.springframework.util.StringUtils;
*/
public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar {
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final String PROXY_DEFAULT_METHODS_ATTR = "proxyDefaultMethods";
@Override
@@ -72,11 +83,14 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
annotationAttributes.put("serviceInterface", importingClassMetadata.getClassName());
annotationAttributes.put(PROXY_DEFAULT_METHODS_ATTR,
"" + annotationAttributes.remove(PROXY_DEFAULT_METHODS_ATTR));
BeanDefinitionReaderUtils.registerBeanDefinition(parse(annotationAttributes), registry);
BeanDefinitionReaderUtils.registerBeanDefinition(gatewayProxyBeanDefinition(annotationAttributes, registry),
registry);
}
}
public BeanDefinitionHolder parse(Map<String, Object> gatewayAttributes) { // NOSONAR complexity
public BeanDefinitionHolder gatewayProxyBeanDefinition(Map<String, Object> gatewayAttributes,
BeanDefinitionRegistry registry) {
String defaultPayloadExpression = (String) gatewayAttributes.get("defaultPayloadExpression");
@SuppressWarnings("unchecked")
@@ -98,91 +112,145 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
Assert.state(!hasMapper || !hasDefaultHeaders,
"'defaultHeaders' are not allowed when a 'mapper' is provided");
BeanDefinitionBuilder gatewayProxyBuilder =
BeanDefinitionBuilder.genericBeanDefinition(GatewayProxyFactoryBean.class);
ConfigurableBeanFactory beanFactory = obtainBeanFactory(registry);
EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver(beanFactory);
Class<?> serviceInterface = getServiceInterface((String) gatewayAttributes.get("serviceInterface"), beanFactory);
if (hasDefaultHeaders || hasDefaultPayloadExpression) {
BeanDefinitionBuilder methodMetadataBuilder =
BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodMetadata.class);
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.");
@SuppressWarnings("unchecked")
AbstractBeanDefinition beanDefinition = new RootBeanDefinition(GatewayProxyFactoryBean.class,
() -> {
GatewayProxyFactoryBean proxyFactoryBean = new GatewayProxyFactoryBean(serviceInterface);
if (StringUtils.hasText(defaultRequestChannel)) {
proxyFactoryBean.setDefaultRequestChannelName(defaultRequestChannel);
}
if (StringUtils.hasText(defaultReplyChannel)) {
proxyFactoryBean.setDefaultReplyChannelName(defaultReplyChannel);
}
if (StringUtils.hasText(errorChannel)) {
proxyFactoryBean.setErrorChannelName(errorChannel);
}
if (StringUtils.hasText(proxyDefaultMethods)) {
boolean actualProxyDefaultMethods =
Boolean.parseBoolean(embeddedValueResolver.resolveStringValue(proxyDefaultMethods));
proxyFactoryBean.setProxyDefaultMethods(actualProxyDefaultMethods);
}
if (StringUtils.hasText(mapper)) {
proxyFactoryBean.setMapper(beanFactory.getBean(mapper, MethodArgsMessageMapper.class));
}
BeanDefinition expressionDef =
new RootBeanDefinition(hasValue ? LiteralExpression.class : ExpressionFactoryBean.class);
expressionDef.getConstructorArgumentValues()
.addGenericArgumentValue(hasValue ? headerValue : headerExpression);
if (asyncExecutor == null || AnnotationConstants.NULL.equals(asyncExecutor)) {
proxyFactoryBean.setAsyncExecutor(null);
}
else if (StringUtils.hasText(asyncExecutor)) {
proxyFactoryBean.setAsyncExecutor(beanFactory.getBean(asyncExecutor, Executor.class));
}
headerExpressions.put((String) header.get("name"), expressionDef);
}
methodMetadataBuilder.addPropertyValue("headerExpressions", headerExpressions);
}
if (hasDefaultHeaders || hasDefaultPayloadExpression) {
GatewayMethodMetadata globalMethodMetadata =
createGlobalMethodMetadata(defaultPayloadExpression, defaultHeaders,
hasDefaultPayloadExpression, hasDefaultHeaders, embeddedValueResolver);
proxyFactoryBean.setGlobalMethodMetadata(globalMethodMetadata);
}
gatewayProxyBuilder.addPropertyValue("globalMethodMetadata", methodMetadataBuilder.getBeanDefinition());
}
Map<String, AbstractBeanDefinition> methodDefinitions =
(Map<String, AbstractBeanDefinition>) gatewayAttributes.get("methods");
if (methodDefinitions != null) {
Map<String, GatewayMethodMetadata> methodMetadataMap =
methodDefinitions.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey,
entry -> (GatewayMethodMetadata) entry.getValue().getInstanceSupplier().get()));
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);
}
proxyFactoryBean.setMethodMetadataMap(methodMetadataMap);
}
gatewayProxyBuilder.addPropertyValue("defaultRequestTimeoutExpressionString",
gatewayAttributes.get("defaultRequestTimeout"));
gatewayProxyBuilder.addPropertyValue("defaultReplyTimeoutExpressionString",
gatewayAttributes.get("defaultReplyTimeout"));
gatewayProxyBuilder.addPropertyValue("methodMetadataMap", gatewayAttributes.get("methods"));
String actualDefaultRequestTimeout =
embeddedValueResolver.resolveStringValue(
(String) gatewayAttributes.get("defaultRequestTimeout"));
if (actualDefaultRequestTimeout != null) {
proxyFactoryBean.setDefaultRequestTimeoutExpressionString(actualDefaultRequestTimeout);
}
String actualDefaultReplyTimeout =
embeddedValueResolver.resolveStringValue(
(String) gatewayAttributes.get("defaultReplyTimeout"));
if (actualDefaultReplyTimeout != null) {
proxyFactoryBean.setDefaultReplyTimeoutExpressionString(actualDefaultReplyTimeout);
}
return proxyFactoryBean;
});
String serviceInterface = (String) gatewayAttributes.get("serviceInterface");
if (!StringUtils.hasText(serviceInterface)) {
serviceInterface = "org.springframework.integration.gateway.RequestReplyExchanger";
}
String id = (String) gatewayAttributes.get("name");
if (!StringUtils.hasText(id)) {
id = Introspector.decapitalize(serviceInterface.substring(serviceInterface.lastIndexOf('.') + 1));
String serviceInterfaceName = serviceInterface.getName();
id = Introspector.decapitalize(serviceInterfaceName.substring(serviceInterfaceName.lastIndexOf('.') + 1));
}
gatewayProxyBuilder.addConstructorArgValue(serviceInterface);
AbstractBeanDefinition beanDefinition = gatewayProxyBuilder.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, serviceInterface);
return new BeanDefinitionHolder(beanDefinition, id);
}
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 org.springframework.integration.gateway.RequestReplyExchanger.class;
}
try {
return ClassUtils.forName(actualServiceInterface, beanFactory.getBeanClassLoader());
}
catch (ClassNotFoundException ex) {
throw new BeanDefinitionStoreException("Cannot parse class for service interface", ex);
}
}
private static GatewayMethodMetadata createGlobalMethodMetadata(String defaultPayloadExpression,
Map<String, Object>[] defaultHeaders, boolean hasDefaultPayloadExpression,
boolean hasDefaultHeaders, EmbeddedValueResolver embeddedValueResolver) {
GatewayMethodMetadata gatewayMethodMetadata = new GatewayMethodMetadata();
if (hasDefaultPayloadExpression) {
String actualPayloadExpression = embeddedValueResolver.resolveStringValue(defaultPayloadExpression);
gatewayMethodMetadata.setPayloadExpression(EXPRESSION_PARSER.parseExpression(actualPayloadExpression));
}
if (hasDefaultHeaders) {
Map<String, Expression> headerExpressions = new HashMap<>();
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.");
}
Expression expression =
hasValue
? new LiteralExpression(embeddedValueResolver.resolveStringValue(headerValue))
: EXPRESSION_PARSER.parseExpression(
embeddedValueResolver.resolveStringValue(headerExpression));
headerExpressions.put((String) header.get("name"), expression);
}
gatewayMethodMetadata.setHeaderExpressions(headerExpressions);
}
return gatewayMethodMetadata;
}
/**
* TODO until SPR-11710 will be resolved.
* Captures the meta-annotation attribute values, in order.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -25,13 +25,20 @@ import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.EmbeddedValueResolver;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
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.integration.config.ExpressionFactoryBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.config.MessagingGatewayRegistrar;
import org.springframework.integration.gateway.GatewayMethodMetadata;
import org.springframework.util.Assert;
@@ -49,6 +56,8 @@ import org.springframework.util.xml.DomUtils;
*/
public class GatewayParser implements BeanDefinitionParser {
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final MessagingGatewayRegistrar registrar = new MessagingGatewayRegistrar();
@Override
@@ -88,7 +97,8 @@ public class GatewayParser implements BeanDefinitionParser {
gatewayAttributes.put("proxyDefaultMethods", element.getAttribute("proxy-default-methods"));
BeanDefinitionHolder gatewayHolder = this.registrar.parse(gatewayAttributes);
BeanDefinitionHolder gatewayHolder =
this.registrar.gatewayProxyBeanDefinition(gatewayAttributes, parserContext.getRegistry());
if (isNested) {
return gatewayHolder.getBeanDefinition();
}
@@ -116,51 +126,81 @@ public class GatewayParser implements BeanDefinitionParser {
private void methods(final 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<>();
ConfigurableBeanFactory beanFactory = obtainBeanFactory(parserContext.getRegistry());
Map<String, AbstractBeanDefinition> methodMetadataMap = new HashMap<>();
for (Element methodElement : methodElements) {
String methodName = methodElement.getAttribute(AbstractBeanDefinitionParser.NAME_ATTRIBUTE);
BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(
GatewayMethodMetadata.class);
methodMetadataBuilder.addPropertyValue("requestChannelName",
methodElement.getAttribute("request-channel"));
methodMetadataBuilder.addPropertyValue("replyChannelName", methodElement.getAttribute("reply-channel"));
methodMetadataBuilder.addPropertyValue("requestTimeout", methodElement.getAttribute("request-timeout"));
methodMetadataBuilder.addPropertyValue("replyTimeout", methodElement.getAttribute("reply-timeout"));
boolean hasMapper = StringUtils.hasText(element.getAttribute("mapper"));
String payloadExpression = methodElement.getAttribute("payload-expression");
Assert.state(!hasMapper || !StringUtils.hasText(payloadExpression),
"'payload-expression' is not allowed when a 'mapper' is provided");
AbstractBeanDefinition gatewayMethodMetadataBd =
new RootBeanDefinition(GatewayMethodMetadata.class,
() -> createGatewayMethodMetadata(element, beanFactory, methodElement));
if (StringUtils.hasText(payloadExpression)) {
methodMetadataBuilder.addPropertyValue("payloadExpression",
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(payloadExpression)
.getBeanDefinition());
}
List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header");
if (!CollectionUtils.isEmpty(invocationHeaders)) {
Assert.state(!hasMapper, "header elements are not allowed when a 'mapper' is provided");
Map<String, Object> headerExpressions = new ManagedMap<>();
for (Element headerElement : invocationHeaders) {
BeanDefinition expressionDef = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext,
headerElement, true);
headerExpressions.put(headerElement.getAttribute(AbstractBeanDefinitionParser.NAME_ATTRIBUTE),
expressionDef);
}
methodMetadataBuilder.addPropertyValue("headerExpressions", headerExpressions);
}
methodMetadataMap.put(methodName, methodMetadataBuilder.getBeanDefinition());
methodMetadataMap.put(methodName, gatewayMethodMetadataBd);
}
gatewayAttributes.put("methods", methodMetadataMap);
}
}
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 GatewayMethodMetadata createGatewayMethodMetadata(Element element,
ConfigurableBeanFactory beanFactory, Element methodElement) {
EmbeddedValueResolver embeddedValueResolver = new EmbeddedValueResolver(beanFactory);
GatewayMethodMetadata gatewayMethodMetadata = new GatewayMethodMetadata();
gatewayMethodMetadata.setRequestChannelName(methodElement.getAttribute("request-channel"));
gatewayMethodMetadata.setReplyChannelName(methodElement.getAttribute("reply-channel"));
gatewayMethodMetadata.setRequestTimeout(
embeddedValueResolver.resolveStringValue(methodElement.getAttribute("request-timeout")));
gatewayMethodMetadata.setReplyTimeout(
embeddedValueResolver.resolveStringValue(methodElement.getAttribute("reply-timeout")));
boolean hasMapper = StringUtils.hasText(element.getAttribute("mapper"));
String payloadExpression = methodElement.getAttribute("payload-expression");
Assert.state(!hasMapper || !StringUtils.hasText(payloadExpression),
"'payload-expression' is not allowed when a 'mapper' is provided");
if (StringUtils.hasText(payloadExpression)) {
gatewayMethodMetadata.setPayloadExpression(
EXPRESSION_PARSER.parseExpression(embeddedValueResolver.resolveStringValue(payloadExpression)));
}
List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header");
if (!CollectionUtils.isEmpty(invocationHeaders)) {
Assert.state(!hasMapper, "header elements are not allowed when a 'mapper' is provided");
Map<String, Expression> headerExpressions = new HashMap<>();
for (Element headerElement : invocationHeaders) {
String headerValue = headerElement.getAttribute("value");
String headerExpression = headerElement.getAttribute("expression");
Expression expression =
StringUtils.hasText(headerValue)
? new LiteralExpression(embeddedValueResolver.resolveStringValue(headerValue))
: EXPRESSION_PARSER.parseExpression(
embeddedValueResolver.resolveStringValue(headerExpression));
headerExpressions.put(headerElement.getAttribute(AbstractBeanDefinitionParser.NAME_ATTRIBUTE),
expression);
}
gatewayMethodMetadata.setHeaderExpressions(headerExpressions);
}
return gatewayMethodMetadata;
}
}

View File

@@ -181,7 +181,7 @@ public class GatewayParserTests {
ConfigurableListableBeanFactory beanFactory = ((GenericApplicationContext) context).getBeanFactory();
Object attribute =
beanFactory.getMergedBeanDefinition("&oneWay").getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
assertThat(attribute).isEqualTo(TestService.class.getName());
assertThat(attribute).isEqualTo(TestService.class);
}
@Test
@@ -189,7 +189,7 @@ public class GatewayParserTests {
ConfigurableListableBeanFactory beanFactory = ((GenericApplicationContext) context).getBeanFactory();
Object attribute =
beanFactory.getMergedBeanDefinition("&defaultConfig").getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
assertThat(attribute).isEqualTo(RequestReplyExchanger.class.getName());
assertThat(attribute).isEqualTo(RequestReplyExchanger.class);
}
@Test