INT-2629 Extract Gateway Argument Mapping Strategy

https://jira.springsource.org/browse/INT-2629

Strategy for mapping gateway methods to map method arguments
to a message.

Default strategy works as today, but pluggable using attribute
'mapper' on <gateway/>.

A custom mapper should implement InboundMessageMapper<MethodArgsHolder>

where the MethodArgsHolder contains the Method object and the argument
values.

When using a custom mapper, the mapper is entirely responsible for
creating the Message - therefore 'payload-expression' attributes
and <header/> elements are not allowed.

INT-2629 Polishing - Javadocs

INT-2629 Polishing

- Introduce MethodArgsMessageMapper - higher level API for custom mapper.

INT-2629: Polishing docs
This commit is contained in:
Gary Russell
2013-10-25 14:43:13 +03:00
committed by Artem Bilan
parent f0c4bdb756
commit 182b232fda
11 changed files with 319 additions and 87 deletions

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser;
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -43,7 +44,7 @@ import org.springframework.util.xml.DomUtils;
public class GatewayParser extends AbstractSimpleBeanDefinitionParser {
private static String[] referenceAttributes = new String[] {
"default-request-channel", "default-reply-channel", "error-channel", "message-mapper", "async-executor"
"default-request-channel", "default-reply-channel", "error-channel", "message-mapper", "async-executor", "mapper"
};
private static String[] innerAttributes = new String[] {
@@ -93,9 +94,16 @@ public class GatewayParser extends AbstractSimpleBeanDefinitionParser {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, attributeName);
}
boolean hasMapper = StringUtils.hasText(element.getAttribute("mapper"));
boolean hasDefaultPayloadExpression = StringUtils.hasText(element.getAttribute("default-payload-expression"));
Assert.state(hasMapper ? !hasDefaultPayloadExpression : true, "'default-payload-expression' is not allowed when a 'mapper' is provided");
List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(element, "default-header");
if (!CollectionUtils.isEmpty(invocationHeaders)
|| StringUtils.hasText(element.getAttribute("default-payload-expression"))) {
boolean hasDefaultHeaders = !CollectionUtils.isEmpty(invocationHeaders);
Assert.state(hasMapper ? !hasDefaultHeaders : true, "default-header elements are not allowed when a 'mapper' is provided");
if (hasDefaultHeaders || hasDefaultPayloadExpression) {
BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.gateway.GatewayMethodMetadata");
this.setMethodInvocationHeaders(methodMetadataBuilder, invocationHeaders);
@@ -118,8 +126,11 @@ public class GatewayParser extends AbstractSimpleBeanDefinitionParser {
methodMetadataBuilder.addPropertyValue("requestTimeout", methodElement.getAttribute("request-timeout"));
methodMetadataBuilder.addPropertyValue("replyTimeout", methodElement.getAttribute("reply-timeout"));
IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, methodElement, "payload-expression");
Assert.state(hasMapper ? !StringUtils.hasText(element.getAttribute("payload-expression")) : true,
"'payload-expression' is not allowed when a 'mapper' is provided");
invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header");
if (!CollectionUtils.isEmpty(invocationHeaders)) {
Assert.state(!hasMapper, "header elements are not allowed when a 'mapper' is provided");
this.setMethodInvocationHeaders(methodMetadataBuilder, invocationHeaders);
}
methodMetadataMap.put(methodName, methodMetadataBuilder.getBeanDefinition());

View File

@@ -42,6 +42,7 @@ import org.springframework.integration.annotation.Headers;
import org.springframework.integration.annotation.Payload;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.mapping.InboundMessageMapper;
import org.springframework.integration.mapping.MessageMappingException;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -85,6 +86,8 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private final List<MethodParameter> parameterList;
private final MethodArgsMessageMapper argsMapper;
private volatile Expression payloadExpression;
private final Map<String, Expression> parameterPayloadExpressions = new HashMap<String, Expression>();
@@ -93,23 +96,28 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private volatile BeanFactory beanFactory;
public GatewayMethodInboundMessageMapper(Method method) {
this(method, null);
}
public GatewayMethodInboundMessageMapper(Method method, Map<String, Expression> headerExpressions) {
this(method, headerExpressions, null);
this(method, headerExpressions, null, null);
}
public GatewayMethodInboundMessageMapper(Method method, Map<String, Expression> headerExpressions,
Map<String, Expression> globalHeaderExpressions) {
Map<String, Expression> globalHeaderExpressions, MethodArgsMessageMapper mapper) {
Assert.notNull(method, "method must not be null");
this.method = method;
this.headerExpressions = headerExpressions;
this.globalHeaderExpressions = globalHeaderExpressions;
this.parameterList = getMethodParameterList(method);
this.payloadExpression = parsePayloadExpression(method);
if (mapper == null) {
this.argsMapper = new DefaultMethodArgsMessageMapper();
}
else {
this.argsMapper = mapper;
}
}
@@ -135,85 +143,17 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
}
private Message<?> mapArgumentsToMessage(Object[] arguments) {
Object messageOrPayload = null;
boolean foundPayloadAnnotation = false;
Map<String, Object> headers = new HashMap<String, Object>();
EvaluationContext methodInvocationEvaluationContext = createMethodInvocationEvaluationContext(arguments);
if (this.payloadExpression != null) {
messageOrPayload = this.payloadExpression.getValue(methodInvocationEvaluationContext);
try {
return this.argsMapper.toMessage(new MethodArgsHolder(this.method, arguments));
}
for (int i = 0; i < this.parameterList.size(); i++) {
Object argumentValue = arguments[i];
MethodParameter methodParameter = this.parameterList.get(i);
Annotation annotation = this.findMappingAnnotation(methodParameter.getParameterAnnotations());
if (annotation != null) {
if (annotation.annotationType().equals(Payload.class)) {
if (messageOrPayload != null) {
this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
}
String expression = ((Payload) annotation).value();
if (!StringUtils.hasText(expression)) {
messageOrPayload = argumentValue;
}
else {
messageOrPayload = this.evaluatePayloadExpression(expression, argumentValue);
}
foundPayloadAnnotation = true;
}
else if (annotation.annotationType().equals(Header.class)) {
Header headerAnnotation = (Header) annotation;
String headerName = this.determineHeaderName(headerAnnotation, methodParameter);
if (headerAnnotation.required() && argumentValue == null) {
throw new IllegalArgumentException("Received null argument value for required header: '" + headerName + "'");
}
headers.put(headerName, argumentValue);
}
else if (annotation.annotationType().equals(Headers.class)) {
if (argumentValue != null) {
if (!(argumentValue instanceof Map)) {
throw new IllegalArgumentException("@Headers annotation is only valid for Map-typed parameters");
}
for (Object key : ((Map<?, ?>) argumentValue).keySet()) {
Assert.isInstanceOf(String.class, key, "Invalid header name [" + key +
"], name type must be String.");
Object value = ((Map<?, ?>) argumentValue).get(key);
headers.put((String) key, value);
}
}
}
catch (Exception e) {
if (e instanceof MessagingException) {
throw (MessagingException) e;
}
else if (messageOrPayload == null) {
messageOrPayload = argumentValue;
}
else if (Map.class.isAssignableFrom(methodParameter.getParameterType())) {
if (messageOrPayload instanceof Map && !foundPayloadAnnotation) {
if (payloadExpression == null){
throw new MessagingException("Ambiguous method parameters; found more than one " +
"Map-typed parameter and neither one contains a @Payload annotation");
}
}
this.copyHeaders((Map<?, ?>) argumentValue, headers);
}
else if (this.payloadExpression == null) {
this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
else {
throw new MessageMappingException("Failed to map arguments", e);
}
}
Assert.isTrue(messageOrPayload != null, "unable to determine a Message or payload parameter on method [" + method + "]");
MessageBuilder<?> builder = (messageOrPayload instanceof Message)
? MessageBuilder.fromMessage((Message<?>) messageOrPayload)
: MessageBuilder.withPayload(messageOrPayload);
builder.copyHeadersIfAbsent(headers);
// Explicit headers in XML override any @Header annotations...
if (!CollectionUtils.isEmpty(this.headerExpressions)) {
Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext, this.headerExpressions);
builder.copyHeaders(evaluatedHeaders);
}
// ...whereas global (default) headers do not...
if (!CollectionUtils.isEmpty(this.globalHeaderExpressions)) {
Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext, this.globalHeaderExpressions);
builder.copyHeadersIfAbsent(evaluatedHeaders);
}
return builder.build();
}
private Map<String, Object> evaluateHeaders(EvaluationContext methodInvocationEvaluationContext, Map<String, Expression> headerExpressions) {
@@ -318,4 +258,94 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
return expression;
}
public class DefaultMethodArgsMessageMapper implements MethodArgsMessageMapper {
@Override
public Message<?> toMessage(MethodArgsHolder holder) throws Exception {
Object messageOrPayload = null;
boolean foundPayloadAnnotation = false;
Object[] arguments = holder.getArgs();
EvaluationContext methodInvocationEvaluationContext = createMethodInvocationEvaluationContext(arguments);
Map<String, Object> headers = new HashMap<String, Object>();
if (GatewayMethodInboundMessageMapper.this.payloadExpression != null) {
messageOrPayload = GatewayMethodInboundMessageMapper.this.payloadExpression.getValue(methodInvocationEvaluationContext);
}
for (int i = 0; i < GatewayMethodInboundMessageMapper.this.parameterList.size(); i++) {
Object argumentValue = arguments[i];
MethodParameter methodParameter = GatewayMethodInboundMessageMapper.this.parameterList.get(i);
Annotation annotation = GatewayMethodInboundMessageMapper.this.findMappingAnnotation(methodParameter.getParameterAnnotations());
if (annotation != null) {
if (annotation.annotationType().equals(Payload.class)) {
if (messageOrPayload != null) {
GatewayMethodInboundMessageMapper.this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
}
String expression = ((Payload) annotation).value();
if (!StringUtils.hasText(expression)) {
messageOrPayload = argumentValue;
}
else {
messageOrPayload = GatewayMethodInboundMessageMapper.this.evaluatePayloadExpression(expression, argumentValue);
}
foundPayloadAnnotation = true;
}
else if (annotation.annotationType().equals(Header.class)) {
Header headerAnnotation = (Header) annotation;
String headerName = GatewayMethodInboundMessageMapper.this.determineHeaderName(headerAnnotation, methodParameter);
if (headerAnnotation.required() && argumentValue == null) {
throw new IllegalArgumentException("Received null argument value for required header: '" + headerName + "'");
}
headers.put(headerName, argumentValue);
}
else if (annotation.annotationType().equals(Headers.class)) {
if (argumentValue != null) {
if (!(argumentValue instanceof Map)) {
throw new IllegalArgumentException("@Headers annotation is only valid for Map-typed parameters");
}
for (Object key : ((Map<?, ?>) argumentValue).keySet()) {
Assert.isInstanceOf(String.class, key, "Invalid header name [" + key +
"], name type must be String.");
Object value = ((Map<?, ?>) argumentValue).get(key);
headers.put((String) key, value);
}
}
}
}
else if (messageOrPayload == null) {
messageOrPayload = argumentValue;
}
else if (Map.class.isAssignableFrom(methodParameter.getParameterType())) {
if (messageOrPayload instanceof Map && !foundPayloadAnnotation) {
if (payloadExpression == null){
throw new MessagingException("Ambiguous method parameters; found more than one " +
"Map-typed parameter and neither one contains a @Payload annotation");
}
}
GatewayMethodInboundMessageMapper.this.copyHeaders((Map<?, ?>) argumentValue, headers);
}
else if (GatewayMethodInboundMessageMapper.this.payloadExpression == null) {
GatewayMethodInboundMessageMapper.this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
}
}
Assert.isTrue(messageOrPayload != null, "unable to determine a Message or payload parameter on method [" + method + "]");
MessageBuilder<?> builder = (messageOrPayload instanceof Message)
? MessageBuilder.fromMessage((Message<?>) messageOrPayload)
: MessageBuilder.withPayload(messageOrPayload);
builder.copyHeadersIfAbsent(headers);
// Explicit headers in XML override any @Header annotations...
if (!CollectionUtils.isEmpty(GatewayMethodInboundMessageMapper.this.headerExpressions)) {
Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext,
GatewayMethodInboundMessageMapper.this.headerExpressions);
builder.copyHeaders(evaluatedHeaders);
}
// ...whereas global (default) headers do not...
if (!CollectionUtils.isEmpty(GatewayMethodInboundMessageMapper.this.globalHeaderExpressions)) {
Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext,
GatewayMethodInboundMessageMapper.this.globalHeaderExpressions);
builder.copyHeadersIfAbsent(evaluatedHeaders);
}
return builder.build();
}
}
}

View File

@@ -105,6 +105,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
private volatile GatewayMethodMetadata globalMethodMetadata;
private volatile MethodArgsMessageMapper argsMapper;
/**
* Create a Factory whose service interface type can be configured by setter injection.
* If none is set, it will fall back to the default service interface type,
@@ -214,6 +216,15 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
this.beanClassLoader = beanClassLoader;
}
/**
* Provide a custom {@link MethodArgsMessageMapper} to map from a {@link MethodArgsHolder}
* to a {@link Message}.
* @param mapper the mapper.
*/
public final void setMapper(MethodArgsMessageMapper mapper) {
this.argsMapper = mapper;
}
@Override
protected void onInit() {
synchronized (this.initializationMonitor) {
@@ -395,7 +406,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
}
}
GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method, headerExpressions,
this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null);
this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null,
this.argsMapper);
if (StringUtils.hasText(payloadExpression)) {
messageMapper.setPayloadExpression(payloadExpression);
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2013 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
*
* http://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.gateway;
import java.lang.reflect.Method;
/**
* Simple wrapper class containing a {@link Method} and an object
* array containing the arguments for an invocation of that method.
* For example used by a {@link MethodArgsMessageMapper} with this generic
* type to provide custom argument mapping when creating a message
* in a {@code GatewayProxyFactoryBean}.
*
* @author Gary Russell
* @since 3.0
*
*/
public final class MethodArgsHolder {
private final Method method;
private final Object[] args;
public MethodArgsHolder(Method method, Object[] args) {
this.method = method;
this.args = args;
}
public final Method getMethod() {
return method;
}
public final Object[] getArgs() {
return args;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2013 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
*
* http://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.gateway;
import org.springframework.integration.mapping.InboundMessageMapper;
/**
* Implementations of this interface are {@link InboundMessageMapper}s
* that map a {@link MethodArgsHolder} to a {@link org.springframework.integration.Message}.
*
* @author Gary Russell
* @since 3.0
*
*/
public interface MethodArgsMessageMapper extends InboundMessageMapper<MethodArgsHolder> {
}

View File

@@ -717,6 +717,22 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="mapper" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
<![CDATA[
An MethodArgsMessageMapper to map the method arguments to a Message. When this
is provided, no payload-expressions or headers are allowed; the custom mapper is
responsible for creating the message.
]]>
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.gateway.MethodArgsMessageMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>

View File

@@ -20,4 +20,10 @@
<int:channel id="requestChannelBar"/>
<int:channel id="requestChannelBaz"/>
<int:gateway id="customMappedGateway"
service-interface="org.springframework.integration.gateway.GatewayInterfaceTests.Baz"
default-request-channel="requestChannelBaz" mapper="mapper"/>
<bean id="mapper" class="org.springframework.integration.gateway.GatewayInterfaceTests$BazMapper"/>
</beans>

View File

@@ -40,6 +40,7 @@ import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.Header;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.support.MessageBuilder;
/**
* @author Oleg Zhurakousky
@@ -238,6 +239,26 @@ public class GatewayInterfaceTests {
new GatewayProxyFactoryBean(NotAnInterface.class);
}
@Test
public void testWithCustomMapper() {
ApplicationContext ac = new ClassPathXmlApplicationContext("GatewayInterfaceTests-context.xml", this.getClass());
DirectChannel channel = ac.getBean("requestChannelBaz", DirectChannel.class);
final AtomicBoolean called = new AtomicBoolean();
MessageHandler handler = new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
assertThat((String) message.getPayload(), equalTo("fizbuz"));
called.set(true);
}
};
channel.subscribe(handler);
Baz baz = ac.getBean(Baz.class);
baz.baz("hello");
assertTrue(called.get());
}
public interface Foo {
@Gateway(requestChannel="requestChannelFoo")
@@ -256,4 +277,18 @@ public class GatewayInterfaceTests {
public static class NotAnInterface {
public void fail(String payload){}
}
public interface Baz {
public void baz(String payload);
}
public static class BazMapper implements MethodArgsMessageMapper {
@Override
public Message<?> toMessage(MethodArgsHolder object) throws Exception {
return MessageBuilder.withPayload("fizbuz").build();
}
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.integration.Message;
import org.springframework.integration.annotation.Header;
import org.springframework.integration.annotation.Headers;
import org.springframework.integration.annotation.Payload;
import org.springframework.integration.mapping.MessageMappingException;
import org.springframework.integration.support.MessageBuilder;
/**
@@ -78,7 +79,7 @@ public class GatewayMethodInboundMessageMapperToMessageTests {
assertEquals("bar", message.getHeaders().get("foo"));
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = MessageMappingException.class)
public void toMessageWithPayloadAndRequiredHeaderButNullValue() throws Exception {
Method method = TestService.class.getMethod(
"sendPayloadAndHeader", String.class, String.class);
@@ -134,7 +135,7 @@ public class GatewayMethodInboundMessageMapperToMessageTests {
assertEquals("test", message.getPayload());
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = MessageMappingException.class)
public void toMessageWithPayloadAndHeadersMapWithNonStringKey() throws Exception {
Method method = TestService.class.getMethod(
"sendPayloadAndHeadersMap", String.class, Map.class);
@@ -166,7 +167,7 @@ public class GatewayMethodInboundMessageMapperToMessageTests {
assertEquals("bar", message.getHeaders().get("foo"));
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = MessageMappingException.class)
public void toMessageWithMessageParameterAndRequiredHeaderButNullValue() throws Exception {
Method method = TestService.class.getMethod("sendMessageAndHeader", Message.class, String.class);
GatewayMethodInboundMessageMapper mapper = new GatewayMethodInboundMessageMapper(method);
@@ -197,7 +198,7 @@ public class GatewayMethodInboundMessageMapperToMessageTests {
assertNull(message.getHeaders().get("foo"));
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = MessageMappingException.class)
public void noArgs() throws Exception {
Method method = TestService.class.getMethod("noArgs", new Class<?>[] {});
GatewayMethodInboundMessageMapper mapper = new GatewayMethodInboundMessageMapper(method);
@@ -205,7 +206,7 @@ public class GatewayMethodInboundMessageMapperToMessageTests {
mapper.toMessage(new Object[] {});
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = MessageMappingException.class)
public void onlyHeaders() throws Exception {
Method method = TestService.class.getMethod("onlyHeaders", String.class, String.class);
GatewayMethodInboundMessageMapper mapper = new GatewayMethodInboundMessageMapper(method);