Skip to content

Commit 29bebdb

Browse files
authored
GH-3047: Add GatewayProxySpec for Java DSL
Fixes #3047 * Improve `GatewayProxyFactoryBean` to determine the return type of the method call from the interface generic types, when the `serviceInterface` is a `java.util.function.Function` * Propagate `MethodArgsHolder` as a `rootObject` for SpEL evaluations * Deprecate `#gatewayMethod` and `#args` evaluation context variables in favor of `MethodArgsHolder` as root object. They will be removed in the future release and a single `EvaluationContext` will be used for all the gateway expressions * Introduce an `IntegrationFlows.from(Class<?> serviceInterface, Consumer<GatewayProxySpec> endpointConfigurer)` to allow to configure any valid gateway proxy options similar to what we have with the `<gateway>` and `@MessagingGateway`. This way we are very close to consistency between different approaches * * Remove `default` prefix from `GatewayProxySpec` options * Document the change
1 parent c668a04 commit 29bebdb

17 files changed

+548
-115
lines changed

spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,14 @@ public BeanDefinitionHolder parse(Map<String, Object> gatewayAttributes) { // NO
102102
BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodMetadata.class);
103103

104104
if (hasDefaultPayloadExpression) {
105-
methodMetadataBuilder.addPropertyValue("payloadExpression", defaultPayloadExpression);
105+
methodMetadataBuilder.addPropertyValue("payloadExpression",
106+
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
107+
.addConstructorArgValue(defaultPayloadExpression)
108+
.getBeanDefinition());
106109
}
107110

108111
if (hasDefaultHeaders) {
109-
Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
112+
Map<String, Object> headerExpressions = new ManagedMap<>();
110113
for (Map<String, Object> header : defaultHeaders) {
111114
String headerValue = (String) header.get("value");
112115
String headerExpression = (String) header.get("expression");
@@ -181,9 +184,11 @@ else if (StringUtils.hasText(asyncExecutor)) {
181184
* @param importingClassMetadata The importing class metadata
182185
* @return The captured values.
183186
*/
184-
private List<MultiValueMap<String, Object>> captureMetaAnnotationValues(AnnotationMetadata importingClassMetadata) {
187+
private static List<MultiValueMap<String, Object>> captureMetaAnnotationValues(
188+
AnnotationMetadata importingClassMetadata) {
189+
185190
Set<String> directAnnotations = importingClassMetadata.getAnnotationTypes();
186-
List<MultiValueMap<String, Object>> valuesHierarchy = new ArrayList<MultiValueMap<String, Object>>();
191+
List<MultiValueMap<String, Object>> valuesHierarchy = new ArrayList<>();
187192
// Need to grab the values now; see SPR-11710
188193
for (String ann : directAnnotations) {
189194
Set<String> chain = importingClassMetadata.getMetaAnnotationTypes(ann);
@@ -203,8 +208,9 @@ private List<MultiValueMap<String, Object>> captureMetaAnnotationValues(Annotati
203208
* @param valuesHierarchy The values hierarchy in order.
204209
* @param annotationAttributes The current attribute values.
205210
*/
206-
private void replaceEmptyOverrides(List<MultiValueMap<String, Object>> valuesHierarchy,
211+
private static void replaceEmptyOverrides(List<MultiValueMap<String, Object>> valuesHierarchy,
207212
Map<String, Object> annotationAttributes) {
213+
208214
for (Entry<String, Object> entry : annotationAttributes.entrySet()) {
209215
Object value = entry.getValue();
210216
if (!MessagingAnnotationUtils.hasValue(value)) {

spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
3232
import org.springframework.beans.factory.xml.BeanDefinitionParser;
3333
import org.springframework.beans.factory.xml.ParserContext;
34+
import org.springframework.integration.config.ExpressionFactoryBean;
3435
import org.springframework.integration.config.MessagingGatewayRegistrar;
3536
import org.springframework.integration.gateway.GatewayMethodMetadata;
3637
import org.springframework.util.Assert;
@@ -54,7 +55,7 @@ public class GatewayParser implements BeanDefinitionParser {
5455
public BeanDefinition parse(final Element element, ParserContext parserContext) {
5556
boolean isNested = parserContext.isNested();
5657

57-
final Map<String, Object> gatewayAttributes = new HashMap<String, Object>();
58+
final Map<String, Object> gatewayAttributes = new HashMap<>();
5859
gatewayAttributes.put(AbstractBeanDefinitionParser.NAME_ATTRIBUTE,
5960
element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE));
6061
gatewayAttributes.put("defaultPayloadExpression", element.getAttribute("default-payload-expression"));
@@ -95,28 +96,27 @@ public BeanDefinition parse(final Element element, ParserContext parserContext)
9596
}
9697
}
9798

98-
@SuppressWarnings("rawtypes")
99-
private void headers(final Element element, final Map<String, Object> gatewayAttributes) {
99+
private void headers(Element element, Map<String, Object> gatewayAttributes) {
100100
List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "default-header");
101101
if (!CollectionUtils.isEmpty(headerElements)) {
102-
List<Map<String, Object>> headers = new ArrayList<Map<String, Object>>(headerElements.size());
102+
List<Map<String, Object>> headers = new ArrayList<>(headerElements.size());
103103
for (Element e : headerElements) {
104-
Map<String, Object> header = new HashMap<String, Object>();
104+
Map<String, Object> header = new HashMap<>();
105105
header.put(AbstractBeanDefinitionParser.NAME_ATTRIBUTE,
106106
e.getAttribute(AbstractBeanDefinitionParser.NAME_ATTRIBUTE));
107107
header.put("value", e.getAttribute("value"));
108108
header.put("expression", e.getAttribute("expression"));
109109
headers.add(header);
110110
}
111-
gatewayAttributes.put("defaultHeaders", headers.toArray(new Map[0]));
111+
gatewayAttributes.put("defaultHeaders", headers.toArray(new Map<?, ?>[0]));
112112
}
113113
}
114114

115115
private void methods(final Element element, ParserContext parserContext,
116116
final Map<String, Object> gatewayAttributes) {
117117
List<Element> methodElements = DomUtils.getChildElementsByTagName(element, "method");
118118
if (!CollectionUtils.isEmpty(methodElements)) {
119-
Map<String, BeanDefinition> methodMetadataMap = new ManagedMap<String, BeanDefinition>();
119+
Map<String, BeanDefinition> methodMetadataMap = new ManagedMap<>();
120120
for (Element methodElement : methodElements) {
121121
String methodName = methodElement.getAttribute(AbstractBeanDefinitionParser.NAME_ATTRIBUTE);
122122
BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(
@@ -128,17 +128,22 @@ private void methods(final Element element, ParserContext parserContext,
128128
methodMetadataBuilder.addPropertyValue("replyTimeout", methodElement.getAttribute("reply-timeout"));
129129

130130
boolean hasMapper = StringUtils.hasText(element.getAttribute("mapper"));
131-
Assert.state(!hasMapper || !StringUtils.hasText(element.getAttribute("payload-expression")),
131+
String payloadExpression = methodElement.getAttribute("payload-expression");
132+
Assert.state(!hasMapper || !StringUtils.hasText(payloadExpression),
132133
"'payload-expression' is not allowed when a 'mapper' is provided");
133134

134-
IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, methodElement,
135-
"payload-expression");
135+
if (StringUtils.hasText(payloadExpression)) {
136+
methodMetadataBuilder.addPropertyValue("payloadExpression",
137+
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
138+
.addConstructorArgValue(payloadExpression)
139+
.getBeanDefinition());
140+
}
136141

137142
List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header");
138143
if (!CollectionUtils.isEmpty(invocationHeaders)) {
139144
Assert.state(!hasMapper, "header elements are not allowed when a 'mapper' is provided");
140145

141-
Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
146+
Map<String, Object> headerExpressions = new ManagedMap<>();
142147
for (Element headerElement : invocationHeaders) {
143148
BeanDefinition expressionDef = IntegrationNamespaceUtils
144149
.createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext,
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.dsl;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.concurrent.Executor;
22+
import java.util.function.Function;
23+
24+
import org.springframework.expression.Expression;
25+
import org.springframework.expression.spel.standard.SpelExpressionParser;
26+
import org.springframework.integration.annotation.AnnotationConstants;
27+
import org.springframework.integration.channel.DirectChannel;
28+
import org.springframework.integration.expression.FunctionExpression;
29+
import org.springframework.integration.expression.ValueExpression;
30+
import org.springframework.integration.gateway.AnnotationGatewayProxyFactoryBean;
31+
import org.springframework.integration.gateway.GatewayMethodMetadata;
32+
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
33+
import org.springframework.integration.gateway.MethodArgsHolder;
34+
import org.springframework.integration.gateway.MethodArgsMessageMapper;
35+
import org.springframework.lang.Nullable;
36+
import org.springframework.messaging.MessageChannel;
37+
38+
/**
39+
* A builder for the {@link GatewayProxyFactoryBean} options
40+
* when {@link org.springframework.integration.annotation.MessagingGateway} on the service interface cannot be
41+
* declared.
42+
*
43+
* @author Artem Bilan
44+
*
45+
* @since 5.2
46+
*/
47+
public class GatewayProxySpec {
48+
49+
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
50+
51+
private final MessageChannel gatewayRequestChannel = new DirectChannel();
52+
53+
private final GatewayProxyFactoryBean gatewayProxyFactoryBean;
54+
55+
private final GatewayMethodMetadata gatewayMethodMetadata = new GatewayMethodMetadata();
56+
57+
private final Map<String, Expression> headerExpressions = new HashMap<>();
58+
59+
private boolean populateGatewayMethodMetadata;
60+
61+
GatewayProxySpec(Class<?> serviceInterface) {
62+
this.gatewayProxyFactoryBean = new AnnotationGatewayProxyFactoryBean(serviceInterface);
63+
this.gatewayProxyFactoryBean.setDefaultRequestChannel(this.gatewayRequestChannel);
64+
}
65+
66+
/**
67+
* Specify a bean name for the target {@link GatewayProxyFactoryBean}.
68+
* @param beanName the bean name to be used for registering bean for the gateway proxy
69+
* @return current {@link GatewayProxySpec}.
70+
*/
71+
public GatewayProxySpec beanName(@Nullable String beanName) {
72+
if (beanName != null) {
73+
this.gatewayProxyFactoryBean.setBeanName(beanName);
74+
}
75+
return this;
76+
}
77+
78+
/**
79+
* Identifies the default channel the gateway proxy will subscribe to, to receive reply
80+
* {@code Message}s, the payloads of
81+
* which will be converted to the return type of the method signature.
82+
* @param channelName the bean name for {@link MessageChannel}
83+
* @return current {@link GatewayProxySpec}.
84+
* @see GatewayProxyFactoryBean#setDefaultReplyChannel
85+
*/
86+
public GatewayProxySpec replyChannel(String channelName) {
87+
this.gatewayProxyFactoryBean.setDefaultReplyChannelName(channelName);
88+
return this;
89+
}
90+
91+
/**
92+
* Identifies the default channel the gateway proxy will subscribe to, to receive reply
93+
* {@code Message}s, the payloads of
94+
* which will be converted to the return type of the method signature.
95+
* @param replyChannel the {@link MessageChannel} for replies.
96+
* @return current {@link GatewayProxySpec}.
97+
* @see GatewayProxyFactoryBean#setDefaultReplyChannel
98+
*/
99+
public GatewayProxySpec replyChannel(MessageChannel replyChannel) {
100+
this.gatewayProxyFactoryBean.setDefaultReplyChannel(replyChannel);
101+
return this;
102+
}
103+
104+
/**
105+
* Identifies a channel that error messages will be sent to if a failure occurs in the
106+
* gateway's proxy invocation. If no {@code errorChannel} reference is provided, the gateway will
107+
* propagate {@code Exception}s to the caller. To completely suppress {@code Exception}s, provide a
108+
* reference to the {@code nullChannel} here.
109+
* @param errorChannelName the bean name for {@link MessageChannel}
110+
* @return current {@link GatewayProxySpec}.
111+
* @see GatewayProxyFactoryBean#setErrorChannel
112+
*/
113+
public GatewayProxySpec errorChannel(String errorChannelName) {
114+
this.gatewayProxyFactoryBean.setErrorChannelName(errorChannelName);
115+
return this;
116+
}
117+
118+
/**
119+
* Identifies a channel that error messages will be sent to if a failure occurs in the
120+
* gateway's proxy invocation. If no {@code errorChannel} reference is provided, the gateway will
121+
* propagate {@code Exception}s to the caller. To completely suppress {@code Exception}s, provide a
122+
* reference to the {@code nullChannel} here.
123+
* @param errorChannel the {@link MessageChannel} for replies.
124+
* @return current {@link GatewayProxySpec}.
125+
* @see GatewayProxyFactoryBean#setErrorChannel
126+
*/
127+
public GatewayProxySpec errorChannel(MessageChannel errorChannel) {
128+
this.gatewayProxyFactoryBean.setErrorChannel(errorChannel);
129+
return this;
130+
}
131+
132+
/**
133+
* Provides the amount of time dispatcher would wait to send a {@code Message}. This
134+
* timeout would only apply if there is a potential to block in the send call. For
135+
* example if this gateway is hooked up to a {@code QueueChannel}. Value is specified
136+
* in milliseconds.
137+
* @param requestTimeout the timeout for requests in milliseconds.
138+
* @return current {@link GatewayProxySpec}.
139+
* @see GatewayProxyFactoryBean#setDefaultRequestTimeout
140+
*/
141+
public GatewayProxySpec requestTimeout(long requestTimeout) {
142+
this.gatewayProxyFactoryBean.setDefaultRequestTimeout(requestTimeout);
143+
return this;
144+
}
145+
146+
/**
147+
* Allows to specify how long this gateway will wait for the reply {@code Message}
148+
* before returning. By default it will wait indefinitely. {@code null} is returned if
149+
* the gateway times out. Value is specified in milliseconds.
150+
* @param replyTimeout the timeout for replies in milliseconds.
151+
* @return current {@link GatewayProxySpec}.
152+
* @see GatewayProxyFactoryBean#setDefaultReplyTimeout
153+
*/
154+
public GatewayProxySpec replyTimeout(long replyTimeout) {
155+
this.gatewayProxyFactoryBean.setDefaultReplyTimeout(replyTimeout);
156+
return this;
157+
}
158+
159+
/**
160+
* Provide a reference to an implementation of {@link Executor}
161+
* to use for any of the interface methods that have a {@link java.util.concurrent.Future} return type.
162+
* This {@code Executor} will only be used for those async methods; the sync methods
163+
* will be invoked in the caller's thread.
164+
* Use {@link AnnotationConstants#NULL} to specify no async executor - for example
165+
* if your downstream flow returns a {@link java.util.concurrent.Future}.
166+
* @param executor the {@link Executor} to use.
167+
* @return current {@link GatewayProxySpec}.
168+
* @see GatewayProxyFactoryBean#setAsyncExecutor
169+
*/
170+
public GatewayProxySpec asyncExecutor(@Nullable Executor executor) {
171+
this.gatewayProxyFactoryBean.setAsyncExecutor(executor);
172+
return this;
173+
}
174+
175+
/**
176+
* An expression that will be used to generate the {@code payload} for all methods in the service interface
177+
* unless explicitly overridden by a method declaration.
178+
* The root object for evaluation context is {@link MethodArgsHolder}.
179+
* @param expression the SpEL expression for default payload.
180+
* @return current {@link GatewayProxySpec}.
181+
* @see org.springframework.integration.annotation.MessagingGateway#defaultPayloadExpression
182+
*/
183+
public GatewayProxySpec payloadExpression(String expression) {
184+
return payloadExpression(PARSER.parseExpression(expression));
185+
}
186+
187+
/**
188+
* A {@link Function} that will be used to generate the {@code payload} for all methods in the service interface
189+
* unless explicitly overridden by a method declaration.
190+
* @param defaultPayloadFunction the {@link Function} for default payload.
191+
* @return current {@link GatewayProxySpec}.
192+
* @see org.springframework.integration.annotation.MessagingGateway#defaultPayloadExpression
193+
*/
194+
public GatewayProxySpec payloadFunction(Function<MethodArgsHolder, ?> defaultPayloadFunction) {
195+
return payloadExpression(new FunctionExpression<>(defaultPayloadFunction));
196+
}
197+
198+
/**
199+
* An expression that will be used to generate the {@code payload} for all methods in the service interface
200+
* unless explicitly overridden by a method declaration.
201+
* The root object for evaluation context is {@link MethodArgsHolder}.
202+
* a bean resolver is also available, enabling expressions like {@code @someBean(#args)}.
203+
* @param expression the SpEL expression for default payload.
204+
* @return current {@link GatewayProxySpec}.
205+
* @see org.springframework.integration.annotation.MessagingGateway#defaultPayloadExpression
206+
*/
207+
public GatewayProxySpec payloadExpression(Expression expression) {
208+
this.gatewayMethodMetadata.setPayloadExpression(expression);
209+
this.populateGatewayMethodMetadata = true;
210+
return this;
211+
}
212+
213+
/**
214+
* Provides custom message header. The default headers are created for
215+
* all methods on the service-interface (unless overridden by a specific method).
216+
* @param headerName the name ofr the header.
217+
* @param value the static value for the header.
218+
* @return current {@link GatewayProxySpec}.
219+
* @see org.springframework.integration.annotation.MessagingGateway#defaultHeaders
220+
*/
221+
public GatewayProxySpec header(String headerName, Object value) {
222+
return header(headerName, new ValueExpression<>(value));
223+
}
224+
225+
/**
226+
* Provides custom message header. The default headers are created for
227+
* all methods on the service-interface (unless overridden by a specific method).
228+
* @param headerName the name ofr the header.
229+
* @param valueFunction the {@link Function} for the header value.
230+
* @return current {@link GatewayProxySpec}.
231+
* @see org.springframework.integration.annotation.MessagingGateway#defaultHeaders
232+
*/
233+
public GatewayProxySpec header(String headerName, Function<MethodArgsHolder, ?> valueFunction) {
234+
return header(headerName, new FunctionExpression<>(valueFunction));
235+
}
236+
237+
/**
238+
* Provides custom message header. The default headers are created for
239+
* all methods on the service-interface (unless overridden by a specific method).
240+
* This expression-based header can get access to the {@link MethodArgsHolder}
241+
* as a root object for evaluation context.
242+
* @param headerName the name ofr the header.
243+
* @param valueExpression the SpEL expression for the header value.
244+
* @return current {@link GatewayProxySpec}.
245+
* @see org.springframework.integration.annotation.MessagingGateway#defaultHeaders
246+
*/
247+
public GatewayProxySpec header(String headerName, Expression valueExpression) {
248+
this.headerExpressions.put(headerName, valueExpression);
249+
this.populateGatewayMethodMetadata = true;
250+
return this;
251+
}
252+
253+
/**
254+
* An {@link MethodArgsMessageMapper}
255+
* to map the method arguments to a {@link org.springframework.messaging.Message}. When this
256+
* is provided, no {@code payload-expression}s or {@code header}s are allowed; the custom mapper is
257+
* responsible for creating the message.
258+
* @param mapper the {@link MethodArgsMessageMapper} to use.
259+
* @return current {@link GatewayProxySpec}.
260+
* @see GatewayProxyFactoryBean#setMapper(MethodArgsMessageMapper)
261+
*/
262+
public GatewayProxySpec mapper(MethodArgsMessageMapper mapper) {
263+
this.gatewayProxyFactoryBean.setMapper(mapper);
264+
return this;
265+
}
266+
267+
MessageChannel getGatewayRequestChannel() {
268+
return this.gatewayRequestChannel;
269+
}
270+
271+
GatewayProxyFactoryBean getGatewayProxyFactoryBean() {
272+
if (this.populateGatewayMethodMetadata) {
273+
this.gatewayMethodMetadata.setHeaderExpressions(this.headerExpressions);
274+
this.gatewayProxyFactoryBean.setGlobalMethodMetadata(this.gatewayMethodMetadata);
275+
}
276+
return this.gatewayProxyFactoryBean;
277+
}
278+
279+
}

0 commit comments

Comments
 (0)