Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

INT-3297: Add `@MessagingGateway` support #1055

Closed
wants to merge 4 commits into from

2 participants

Artem Bilan Gary Russell
Artem Bilan
Collaborator

JIRA: https://jira.springsource.org/browse/INT-3297

  • Add <gateway> annotation analogue and its MessagingGatewayRegistrar to register GatewayProxyFactoryBean based on annotation attributes
  • Add @IntegrationComponentScan and its IntegrationComponentsRegistrar to scan packages for integration components
  • Add implementation to scan @MessagingGateway
  • Add simple @MessagingGateway test

REVIEW ONLY
There is need:

  • to improve IntegrationComponentsRegistrar, to make it more generic and independent of cocreate Registrar implementation - Visitor pattern
  • Conver GatewayParser to use MessagingGatewayRegistrar. It's allow to check existing tests that everything is correct in the implementation of latest
  • Add more test
  • Add docs
Gary Russell
Owner

Looks good; I wonder if we could make this work somehow...

@MessagingGateway(defaultRequestChannel = "gatewayChannel", defaultRequestTimeout=5000)
public static interface TestGateway {

    String echo(String payload);

}

@MessagingGateway(defaultRequestChannel = "gateway2Channel")
public static interface TestGateway2 extends TestGateway { }

(inherit and merge attributes).

Artem Bilan
Collaborator

:smile: Is there something similar already in somewhere Spring? E.g. Spring Data ?
Let's think about it later. We don't have parent for <gateway> now, so we can live without inheritance some time again.

OK. I'll go ahead tomorrow and bring it to mind

Gary Russell
Owner

It was just a passing thought - in XML, we can declare multiple gateways with the same service-interface with annotation "discovery" we can't do that; so I was just thinking of alternatives.

But, I agree, not important right now.

Artem Bilan
Collaborator

Pushed

Artem Bilan added some commits
Artem Bilan INT-3297: Add `@MessagingGateway` support
JIRA: https://jira.springsource.org/browse/INT-3297

* Add `<gateway>` annotation analogue and its `MessagingGatewayRegistrar` to register `GatewayProxyFactoryBean` based on annotation attributes
* Add `@IntegrationComponentScan` and its `IntegrationComponentsRegistrar` to scan packages for integration components
* Add implementation to scan `@MessagingGateway`
* Add simple `@MessagingGateway` test
a6e0b0b
Artem Bilan INT-3297: Further improvement
* Make `GatewayParser` to delegate the hard work to the `MessagingGatewayRegistrar`
* Add Docs
* Checked all existing tests
35caef8
Artem Bilan INT-3297: Polishing ad5d11e
Artem Bilan
Collaborator

Rebased and poolished

Artem Bilan artembilan commented on the diff
src/reference/docbook/whats-new.xml
((20 lines not shown))
<section id="4.0-enable-configuration">
<title>@EnableIntegration</title>
<para>
The <code>@EnableIntegration</code> annotation has been added, to permit declaration of
- standard Spring Integration beans when using <code>@Configuration</code> classes. See
- <xref linkend="enable-integration"/> for more information.
+ standard Spring Integration beans when using <code>@Configuration</code> classes.
+ See <xref linkend="enable-integration"/> for more information.
+ </para>
+ </section>
+ <section id="4.0-component-scan">
+ <title>@IntegrationComponentScan</title>
+ <para>
+ The <code>@IntegrationComponentScan</code> annotation has been added, to permit classpath
+ scanning for Spring Integration specific components.
+ See <xref linkend="enable-integration"/> for more information.
Artem Bilan Collaborator

Pay attention, please, here to the result link in the PDF: What is that [10] ?

Gary Russell Owner

It's the page number (my PDF viewer - linux) puts up a tool tip when you hover over the link "Go to page 10".

Artem Bilan Collaborator

Let it be, but it is interest why there is no page number with other links?..

Gary Russell Owner

It's because the linkend references a <para/> instead of a <section/>.

A section can run over multiple pages; I think it's more important to jump to the appropriate paragraph than worry about this little inconsistency.

Artem Bilan Collaborator

Sure! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Artem Bilan
Collaborator

Now it's ready for general review and merge

...ngframework/integration/config/xml/GatewayParser.java
((145 lines not shown))
- methodMetadataBuilder.addPropertyValue("replyChannelName", methodElement.getAttribute("reply-channel"));
- 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());
- }
- builder.addPropertyValue("methodMetadataMap", methodMetadataMap);
- }
+ AnnotationMetadata importingClassMetadata = new StandardAnnotationMetadata(GatewayParser.class) {
Gary Russell Owner

Interesting technique, but I wonder if it would be better to simply have an intermediate "GatewayAttributes" class, rather than making the parser dependent on the Annotation classes.

Then, the gateway attributes can be built either by the parser, or from the annotation.

It just seems wrong to me to have the parser synthesize an annotation.

Artem Bilan Collaborator

OK. I think we can just convert it to the Map<String, Object>. As far as MessagingGatewayRegistrar does the same in the parse

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Artem Bilan
Collaborator

Pushed

Gary Russell garyrussell commented on the diff
...gration/config/IntegrationComponentScanRegistrar.java
((84 lines not shown))
+ @Override
+ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
+ return beanDefinition.getMetadata().isIndependent();
+ }
+ };
+
+ for (TypeFilter typeFilter : componentRegistrars.keySet()) {
+ scanner.addIncludeFilter(typeFilter);
+ }
+
+ scanner.setResourceLoader(resourceLoader);
+
+ for (String basePackage : basePackages) {
+ Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
+ for (BeanDefinition candidateComponent : candidateComponents) {
+ if (candidateComponent instanceof AnnotatedBeanDefinition) {
Gary Russell Owner

LGTM; except I wonder if we should add this...

Assert.isTrue(((AnnotatedBeanDefinition) candidateComponent).getMetadata().isInterface(),
                        "@MessagingGateway can only be specified on an interface");

...here. Without it, we get...

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableIntegrationTests$TestGateway2': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.springframework.integration.gateway.GatewayProxyFactoryBean]: Constructor threw exception; nested exception is java.lang.IllegalArgumentException: 'serviceInterface' must be an interface
...
Caused by: java.lang.IllegalArgumentException: 'serviceInterface' must be an interface

... which I suppose is fairly obvious, to those who understand what's happening, but perhaps the Assert would be more helpful to newbies?

WDYT?

Artem Bilan Collaborator

Correct. Thanks. Wouldn't you mind to do it on merge ?

Artem Bilan Collaborator

However it should be in concrete visitor - MessagingGatewayRegistrar

Gary Russell Owner

Ah - ok - will do during merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Gary Russell
Owner

Merged

Gary Russell garyrussell closed this
Artem Bilan artembilan deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 13, 2014
  1. INT-3297: Add `@MessagingGateway` support

    Artem Bilan authored
    JIRA: https://jira.springsource.org/browse/INT-3297
    
    * Add `<gateway>` annotation analogue and its `MessagingGatewayRegistrar` to register `GatewayProxyFactoryBean` based on annotation attributes
    * Add `@IntegrationComponentScan` and its `IntegrationComponentsRegistrar` to scan packages for integration components
    * Add implementation to scan `@MessagingGateway`
    * Add simple `@MessagingGateway` test
  2. INT-3297: Further improvement

    Artem Bilan authored
    * Make `GatewayParser` to delegate the hard work to the `MessagingGatewayRegistrar`
    * Add Docs
    * Checked all existing tests
  3. INT-3297: Polishing

    Artem Bilan authored
  4. INT-3297: Get rid of annotation dep from GWParser

    Artem Bilan authored
This page is out of date. Refresh to see the latest.
Showing with 727 additions and 145 deletions.
  1. +6 −1 spring-integration-core/src/main/java/org/springframework/integration/annotation/Gateway.java
  2. +49 −0 spring-integration-core/src/main/java/org/springframework/integration/annotation/GatewayHeader.java
  3. +64 −0 spring-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java
  4. +125 −0 spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java
  5. +108 −0 ...ntegration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java
  6. +141 −0 spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java
  7. +77 −116 spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java
  8. +35 −5 spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java
  9. +1 −1  ...g-integration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.0.xsd
  10. +38 −1 ...ng-integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java
  11. +1 −1  ...ng-integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java
  12. +40 −0 src/reference/docbook/gateway.xml
  13. +7 −1 src/reference/docbook/overview.xml
  14. +35 −19 src/reference/docbook/whats-new.xml
7 spring-integration-core/src/main/java/org/springframework/integration/annotation/Gateway.java
View
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -44,6 +44,7 @@
*
* @author Mark Fisher
* @author Gary Russell
+ * @author Artem Bilan
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@@ -59,4 +60,8 @@
long replyTimeout() default Long.MIN_VALUE;
+ String payloadExpression() default "";
+
+ GatewayHeader[] headers() default {};
+
}
49 spring-integration-core/src/main/java/org/springframework/integration/annotation/GatewayHeader.java
View
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 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.annotation;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Provides the message header {@code value} or {@code expression}.
+ *
+ * @author Artem Bilan
+ * @since 4.0
+ */
+@Target({ })
+@Retention(RUNTIME)
+public @interface GatewayHeader {
+
+ /**
+ * @return The name of the header.
+ */
+ String name();
+
+ /**
+ * @return The value for the header.
+ */
+ String value() default "";
+
+ /**
+ * @return The {@code Expression} to be evaluated to produce a value for the header.
+ */
+ String expression() default "";
+
+}
64 ...-integration-core/src/main/java/org/springframework/integration/annotation/IntegrationComponentScan.java
View
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.integration.config.IntegrationComponentScanRegistrar;
+
+/**
+ * Configures component scanning directives for use with @{@link org.springframework.context.annotation.Configuration} classes.
+ * Scan Spring Integration specific components.
+ *
+ * @author Artem Bilan
+ * @since 4.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+@Import(IntegrationComponentScanRegistrar.class)
+public @interface IntegrationComponentScan {
+
+ /**
+ * Alias for the {@link #basePackages()} attribute.
+ * Allows for more concise annotation declarations e.g.:
+ * {@code @ComponentScan("org.my.pkg")} instead of
+ * {@code @ComponentScan(basePackages="org.my.pkg")}.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated components.
+ * <p>{@link #value()} is an alias for (and mutually exclusive with) this attribute.
+ * <p>Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages
+ * to scan for annotated components. The package of each class specified will be scanned.
+ * <p>Consider creating a special no-op marker class or interface in each package
+ * that serves no purpose other than being referenced by this attribute.
+ */
+ Class<?>[] basePackageClasses() default {};
+
+}
125 spring-integration-core/src/main/java/org/springframework/integration/annotation/MessagingGateway.java
View
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2014 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.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The stereotype annotation to provide the Integration Messaging Gateway Proxy ({@code <gateway/>}).
+ *
+ * @author Artem Bilan
+ * @since 4.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+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
+ */
+ String name() default "";
+
+ /**
+ * Identifies default channel the messages will be sent to upon invocation of methods of the gateway proxy.
+ *
+ * @return the suggested channel name, if any
+ */
+ String defaultRequestChannel() default "";
+
+ /**
+ * Identifies default channel the gateway proxy will subscribe to to receive reply {@code Message}s, which will then be
+ * converted to the return type of the method signature.
+ *
+ * @return the suggested channel name, if any
+ */
+ String defaultReplyChannel() default "";
+
+ /**
+ * Identifies a channel that error messages will be sent to if a failure occurs in the
+ * gateway's proxy invocation. If no {@code errorChannel} reference is provided, the gateway will
+ * propagate {@code Exception}s to the caller. To completely suppress {@code Exception}s, provide a
+ * reference to the {@code nullChannel} here.
+ *
+ * @return the suggested channel name, if any
+ */
+ String errorChannel() default "";
+
+ /**
+ * Provides the amount of time dispatcher would wait to send a {@code Message}.
+ * This timeout would only apply if there is a potential to block in the send call.
+ * For example if this gateway is hooked up to a {@code QueueChannel}. 
+ *
+ * @return the suggested timeout in milliseconds, if any
+ */
+ long defaultRequestTimeout() default Long.MIN_VALUE;
+
+ /**
+ * 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 the gateway times out.
+ *
+ * @return the suggested timeout in milliseconds, if any
+ */
+ long defaultReplyTimeout() default Long.MIN_VALUE;
+
+ /**
+ * Provide a reference to an implementation of {@link java.util.concurrent.Executor}
+ * to use for any of the interface methods that have a {@link java.util.concurrent.Future} return type.
+ * This {@code Executor} will only be used for those async methods; the sync methods
+ * will be invoked in the caller's thread.
+ *
+ * @return the suggested executor bean name, if any
+ */
+ String asyncExecutor() default "";
+
+ /**
+ * An expression that will be used to generate the {@code payload} for all methods in the service interface
+ * unless explicitly overridden by a method declaration. Variables include {@code #args}, {@code #methodName},
+ * {@code #methodString} and {@code #methodObject};
+ * a bean resolver is also available, enabling expressions like {@code @someBean(#args)}.
+ *
+ * @return the suggested payload expression, if any
+ */
+ String defaultPayloadExpression() default "";
+
+ /**
+ * Provides custom message headers. These default headers are created for
+ * all methods on the service-interface (unless overridden by a specific method).
+ *
+ * @return the suggested payload expression, if any
+ */
+ GatewayHeader[] defaultHeaders() default {};
+
+ /**
+ * An {@link org.springframework.integration.gateway.MethodArgsMessageMapper}
+ * to map the method arguments to a {@link org.springframework.messaging.Message}. When this
+ * is provided, no {@code payload-expression}s or {@code header}s are allowed; the custom mapper is
+ * responsible for creating the message.
+ *
+ * @return the suggested mapper bean name, if any
+ */
+ String mapper() default "";
+
+}
108 ...gration-core/src/main/java/org/springframework/integration/config/IntegrationComponentScanRegistrar.java
View
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2014 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.config;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.ResourceLoaderAware;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.core.type.filter.TypeFilter;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link ImportBeanDefinitionRegistrar} implementation to scan and register Integration specific components.
+ *
+ * @author Artem Bilan
+ * @since 4.0
+ */
+public class IntegrationComponentScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
+
+ private final Map<TypeFilter, ImportBeanDefinitionRegistrar> componentRegistrars = new HashMap<TypeFilter, ImportBeanDefinitionRegistrar>();
+
+ private ResourceLoader resourceLoader;
+
+ public IntegrationComponentScanRegistrar() {
+ this.componentRegistrars.put(new AnnotationTypeFilter(MessagingGateway.class), new MessagingGatewayRegistrar());
+ }
+
+ @Override
+ public void setResourceLoader(ResourceLoader resourceLoader) {
+ this.resourceLoader = resourceLoader;
+ }
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+ Map<String, Object> componentScan = importingClassMetadata.getAnnotationAttributes("org.springframework.integration.annotation.IntegrationComponentScan");
+
+ Set<String> basePackages = new HashSet<String>();
+ for (String pkg : (String[]) componentScan.get("value")) {
+ if (StringUtils.hasText(pkg)) {
+ basePackages.add(pkg);
+ }
+ }
+ for (String pkg : (String[]) componentScan.get("basePackages")) {
+ if (StringUtils.hasText(pkg)) {
+ basePackages.add(pkg);
+ }
+ }
+ for (Class<?> clazz : (Class[]) componentScan.get("basePackageClasses")) {
+ basePackages.add(ClassUtils.getPackageName(clazz));
+ }
+
+ if (basePackages.isEmpty()) {
+ basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
+ }
+
+ ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) {
+
+ @Override
+ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
+ return beanDefinition.getMetadata().isIndependent();
+ }
+ };
+
+ for (TypeFilter typeFilter : componentRegistrars.keySet()) {
+ scanner.addIncludeFilter(typeFilter);
+ }
+
+ scanner.setResourceLoader(resourceLoader);
+
+ for (String basePackage : basePackages) {
+ Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
+ for (BeanDefinition candidateComponent : candidateComponents) {
+ if (candidateComponent instanceof AnnotatedBeanDefinition) {
Gary Russell Owner

LGTM; except I wonder if we should add this...

Assert.isTrue(((AnnotatedBeanDefinition) candidateComponent).getMetadata().isInterface(),
                        "@MessagingGateway can only be specified on an interface");

...here. Without it, we get...

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableIntegrationTests$TestGateway2': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [org.springframework.integration.gateway.GatewayProxyFactoryBean]: Constructor threw exception; nested exception is java.lang.IllegalArgumentException: 'serviceInterface' must be an interface
...
Caused by: java.lang.IllegalArgumentException: 'serviceInterface' must be an interface

... which I suppose is fairly obvious, to those who understand what's happening, but perhaps the Assert would be more helpful to newbies?

WDYT?

Artem Bilan Collaborator

Correct. Thanks. Wouldn't you mind to do it on merge ?

Artem Bilan Collaborator

However it should be in concrete visitor - MessagingGatewayRegistrar

Gary Russell Owner

Ah - ok - will do during merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ for (ImportBeanDefinitionRegistrar importBeanDefinitionRegistrar : componentRegistrars.values()) {
+ importBeanDefinitionRegistrar.registerBeanDefinitions(((AnnotatedBeanDefinition) candidateComponent).getMetadata(), registry);
+ }
+ }
+ }
+ }
+ }
+
+}
141 spring-integration-core/src/main/java/org/springframework/integration/config/MessagingGatewayRegistrar.java
View
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2014 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.config;
+
+import java.beans.Introspector;
+import java.util.Map;
+
+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.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.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.expression.common.LiteralExpression;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.gateway.GatewayMethodMetadata;
+import org.springframework.integration.gateway.GatewayProxyFactoryBean;
+import org.springframework.util.Assert;
+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
+ * @since 4.0
+ */
+public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar {
+
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+ if (importingClassMetadata != null && importingClassMetadata.hasAnnotation(MessagingGateway.class.getName())) {
+ Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(MessagingGateway.class.getName());
+ annotationAttributes.put("serviceInterface", importingClassMetadata.getClassName());
+
+ BeanDefinitionReaderUtils.registerBeanDefinition(this.parse(annotationAttributes), registry);
+ }
+ }
+
+ public BeanDefinitionHolder parse(Map<String, Object> gatewayAttributes) {
+
+ 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");
+
+ 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");
+
+ BeanDefinitionBuilder gatewayProxyBuilder = BeanDefinitionBuilder.genericBeanDefinition(GatewayProxyFactoryBean.class);
+
+ if (hasDefaultHeaders || hasDefaultPayloadExpression) {
+ BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(GatewayMethodMetadata.class);
+ if (hasDefaultPayloadExpression) {
+ methodMetadataBuilder.addPropertyValue("payloadExpression", defaultPayloadExpression);
+ }
+ Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
+ 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.addPropertyReference("defaultRequestChannel", defaultRequestChannel);
+ }
+ if (StringUtils.hasText(defaultReplyChannel)) {
+ gatewayProxyBuilder.addPropertyReference("defaultReplyChannel", defaultReplyChannel);
+ }
+ if (StringUtils.hasText(errorChannel)) {
+ gatewayProxyBuilder.addPropertyReference("errorChannel", errorChannel);
+ }
+ if (StringUtils.hasText(asyncExecutor)) {
+ gatewayProxyBuilder.addPropertyReference("asyncExecutor", asyncExecutor);
+ }
+ if (StringUtils.hasText(mapper)) {
+ gatewayProxyBuilder.addPropertyReference("mapper", mapper);
+ }
+
+ gatewayProxyBuilder.addPropertyValue("defaultRequestTimeout", gatewayAttributes.get("defaultRequestTimeout"));
+ gatewayProxyBuilder.addPropertyValue("defaultReplyTimeout", gatewayAttributes.get("defaultReplyTimeout"));
+ gatewayProxyBuilder.addPropertyValue("methodMetadataMap", gatewayAttributes.get("methods"));
+
+
+ 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));
+ }
+
+ gatewayProxyBuilder.addConstructorArgValue(serviceInterface);
+
+ return new BeanDefinitionHolder(gatewayProxyBuilder.getBeanDefinition(), id);
+ }
+
+}
193 spring-integration-core/src/main/java/org/springframework/integration/config/xml/GatewayParser.java
View
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2014 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.
@@ -16,24 +16,25 @@
package org.springframework.integration.config.xml;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
-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.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.ManagedMap;
-import org.springframework.beans.factory.support.RootBeanDefinition;
-import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser;
-import org.springframework.expression.common.LiteralExpression;
-import org.springframework.integration.config.ExpressionFactoryBean;
+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.MessagingGatewayRegistrar;
import org.springframework.integration.gateway.GatewayMethodMetadata;
-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;
import org.springframework.util.xml.DomUtils;
@@ -43,129 +44,89 @@
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
+ * @author Artem Bilan
*/
-public class GatewayParser extends AbstractSimpleBeanDefinitionParser {
+public class GatewayParser implements BeanDefinitionParser {
- private static String[] referenceAttributes = new String[] {
- "default-request-channel", "default-reply-channel", "error-channel", "message-mapper", "async-executor", "mapper"
- };
-
- private static String[] innerAttributes = new String[] {
- "request-channel", "reply-channel", "request-timeout", "reply-timeout", "error-channel"
- };
-
-
- @Override
- protected String getBeanClassName(Element element) {
- return GatewayProxyFactoryBean.class.getName();
- }
-
- @Override
- protected boolean shouldGenerateIdAsFallback() {
- return true;
- }
+ private final MessagingGatewayRegistrar registrar = new MessagingGatewayRegistrar();
@Override
- protected boolean isEligibleAttribute(String attributeName) {
- return !ObjectUtils.containsElement(referenceAttributes, attributeName)
- && !ObjectUtils.containsElement(innerAttributes, attributeName)
- && !("default-payload-expression".equals(attributeName))
- && super.isEligibleAttribute(attributeName);
- }
-
- @Override
- protected void postProcess(BeanDefinitionBuilder builder, Element element) {
- if ("chain".equals(element.getParentNode().getLocalName())) {
- this.postProcessInnerGateway(builder, element);
- }
- else {
- this.postProcessGateway(builder, element);
+ @SuppressWarnings("rawtypes")
+ public BeanDefinition parse(final Element element, ParserContext parserContext) {
+ boolean isNested = parserContext.isNested();
+
+ final Map<String, Object> gatewayAttributes = new HashMap<String, Object>();
+ gatewayAttributes.put("name", element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE));
+ gatewayAttributes.put("defaultPayloadExpression", element.getAttribute("default-payload-expression"));
+ gatewayAttributes.put("defaultRequestChannel", element.getAttribute(isNested ? "request-channel" : "default-request-channel"));
+ gatewayAttributes.put("defaultReplyChannel", element.getAttribute(isNested ? "reply-channel" : "default-reply-channel"));
+ gatewayAttributes.put("errorChannel", element.getAttribute("error-channel"));
+ gatewayAttributes.put("asyncExecutor", element.getAttribute("async-executor"));
+ gatewayAttributes.put("mapper", element.getAttribute("mapper"));
+ gatewayAttributes.put("defaultReplyTimeout", element.getAttribute(isNested ? "reply-timeout" : "default-reply-timeout"));
+ gatewayAttributes.put("defaultRequestTimeout", element.getAttribute(isNested ? "request-timeout" : "default-request-timeout"));
+
+
+ List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "default-header");
+ if (!CollectionUtils.isEmpty(headerElements)) {
+ List<Map<String, Object>> headers = new ArrayList<Map<String, Object>>(headerElements.size());
+ for (Element e : headerElements) {
+ Map<String, Object> header = new HashMap<String, Object>();
+ header.put("name", e.getAttribute("name"));
+ header.put("value", e.getAttribute("value"));
+ header.put("expression", e.getAttribute("expression"));
+ headers.add(header);
+ }
+ gatewayAttributes.put("defaultHeaders", headers.toArray(new Map[headers.size()]));
}
- }
- private void postProcessInnerGateway(BeanDefinitionBuilder builder, Element element) {
- IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-channel", "defaultRequestChannel");
- IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel", "defaultReplyChannel");
- IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout", "defaultRequestTimeout");
- IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout", "defaultReplyTimeout");
- IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
- IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "async-executor");
- }
+ List<Element> methodElements = DomUtils.getChildElementsByTagName(element, "method");
+ if (!CollectionUtils.isEmpty(methodElements)) {
+ Map<String, BeanDefinition> methodMetadataMap = new ManagedMap<String, BeanDefinition>();
+ for (Element methodElement : methodElements) {
+ String methodName = methodElement.getAttribute("name");
+ 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"));
+ Assert.state(!hasMapper || !StringUtils.hasText(element.getAttribute("payload-expression")),
+ "'payload-expression' is not allowed when a 'mapper' is provided");
- private void postProcessGateway(BeanDefinitionBuilder builder, Element element) {
- for (String attributeName : referenceAttributes) {
- IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, attributeName);
- }
+ IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, methodElement, "payload-expression");
- 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(methodElement, "header");
+ if (!CollectionUtils.isEmpty(invocationHeaders)) {
+ Assert.state(!hasMapper, "header elements are not allowed when a 'mapper' is provided");
- List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(element, "default-header");
- boolean hasDefaultHeaders = !CollectionUtils.isEmpty(invocationHeaders);
+ Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
+ for (Element headerElement : invocationHeaders) {
+ BeanDefinition expressionDef = IntegrationNamespaceUtils
+ .createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext, headerElement, true);
- Assert.state(hasMapper ? !hasDefaultHeaders : true, "default-header elements are not allowed when a 'mapper' is provided");
+ headerExpressions.put(headerElement.getAttribute("name"), expressionDef);
+ }
+ methodMetadataBuilder.addPropertyValue("headerExpressions", headerExpressions);
+ }
+ methodMetadataMap.put(methodName, methodMetadataBuilder.getBeanDefinition());
+ }
- if (hasDefaultHeaders || hasDefaultPayloadExpression) {
- BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(
- GatewayMethodMetadata.class);
- this.setMethodInvocationHeaders(methodMetadataBuilder, invocationHeaders);
- IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, element,
- "default-payload-expression", "payloadExpression");
- builder.addPropertyValue("globalMethodMetadata", methodMetadataBuilder.getBeanDefinition());
+ gatewayAttributes.put("methods", methodMetadataMap);
}
- List<Element> elements = DomUtils.getChildElementsByTagName(element, "method");
- ManagedMap<String, BeanDefinition> methodMetadataMap = null;
- if (elements != null && elements.size() > 0) {
- methodMetadataMap = new ManagedMap<String, BeanDefinition>();
- }
- for (Element methodElement : elements) {
- String methodName = methodElement.getAttribute("name");
- 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"));
- 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());
- }
- builder.addPropertyValue("methodMetadataMap", methodMetadataMap);
- }
+ gatewayAttributes.put("serviceInterface", element.getAttribute("service-interface"));
- private void setMethodInvocationHeaders(BeanDefinitionBuilder gatewayDefinitionBuilder, List<Element> invocationHeaders) {
- Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
- for (Element headerElement : invocationHeaders) {
- String headerName = headerElement.getAttribute("name");
- String headerValue = headerElement.getAttribute("value");
- String headerExpression = headerElement.getAttribute("expression");
- boolean hasValue = StringUtils.hasText(headerValue);
- boolean hasExpression = StringUtils.hasText(headerExpression);
- if (!(hasValue ^ hasExpression)) {
- throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' is required on a header sub-element");
- }
- RootBeanDefinition expressionDef = null;
- if (hasValue) {
- expressionDef = new RootBeanDefinition(LiteralExpression.class);
- expressionDef.getConstructorArgumentValues().addGenericArgumentValue(headerValue);
- }
- else if (hasExpression) {
- expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class);
- expressionDef.getConstructorArgumentValues().addGenericArgumentValue(headerExpression);
- }
- if (expressionDef != null) {
- headerExpressions.put(headerName, expressionDef);
- }
+ BeanDefinitionHolder gatewayHolder = this.registrar.parse(gatewayAttributes);
+ if (isNested) {
+ return gatewayHolder.getBeanDefinition();
+ }
+ else {
+ BeanDefinitionReaderUtils.registerBeanDefinition(gatewayHolder, parserContext.getRegistry());
+ return null;
}
- gatewayDefinitionBuilder.addPropertyValue("headerExpressions", headerExpressions);
}
}
40 spring-integration-core/src/main/java/org/springframework/integration/gateway/GatewayProxyFactoryBean.java
View
@@ -34,6 +34,7 @@
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.convert.ConversionService;
@@ -41,7 +42,10 @@
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.expression.Expression;
+import org.springframework.expression.common.LiteralExpression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.annotation.Gateway;
+import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.annotation.Payload;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.endpoint.AbstractEndpoint;
@@ -54,6 +58,7 @@
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -68,9 +73,12 @@
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
+ * @author Artem Bilan
*/
public class GatewayProxyFactoryBean extends AbstractEndpoint implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {
+ private static final SpelExpressionParser PARSER = new SpelExpressionParser();
+
private volatile Class<?> serviceInterface;
private volatile MessageChannel defaultRequestChannel;
@@ -174,7 +182,7 @@ public void setErrorChannel(MessageChannel errorChannel) {
*
* @param defaultRequestTimeout the timeout value in milliseconds
*/
- public void setDefaultRequestTimeout(long defaultRequestTimeout) {
+ public void setDefaultRequestTimeout(Long defaultRequestTimeout) {
this.defaultRequestTimeout = defaultRequestTimeout;
}
@@ -184,7 +192,7 @@ public void setDefaultRequestTimeout(long defaultRequestTimeout) {
*
* @param defaultReplyTimeout the timeout value in milliseconds
*/
- public void setDefaultReplyTimeout(long defaultReplyTimeout) {
+ public void setDefaultReplyTimeout(Long defaultReplyTimeout) {
this.defaultReplyTimeout = defaultReplyTimeout;
}
@@ -368,7 +376,7 @@ private MethodInvocationGateway createGatewayForMethod(Method method) {
Long replyTimeout = this.defaultReplyTimeout;
String payloadExpression = this.globalMethodMetadata != null ? this.globalMethodMetadata.getPayloadExpression()
: null;
- Map<String, Expression> headerExpressions = null;
+ Map<String, Expression> headerExpressions = new HashMap<String, Expression>();
if (gatewayAnnotation != null) {
String requestChannelName = gatewayAnnotation.requestChannel();
if (StringUtils.hasText(requestChannelName)) {
@@ -391,12 +399,34 @@ private MethodInvocationGateway createGatewayForMethod(Method method) {
if (replyTimeout == null || gatewayAnnotation.replyTimeout() != Long.MIN_VALUE) {
replyTimeout = gatewayAnnotation.replyTimeout();
}
+ if (payloadExpression == null || StringUtils.hasText(gatewayAnnotation.payloadExpression())) {
+ payloadExpression = gatewayAnnotation.payloadExpression();
+ }
+
+ if (!ObjectUtils.isEmpty(gatewayAnnotation.headers())) {
+ for (GatewayHeader gatewayHeader : gatewayAnnotation.headers()) {
+ String value = gatewayHeader.value();
+ String expression = gatewayHeader.expression();
+ String name = gatewayHeader.name();
+ boolean hasValue = StringUtils.hasText(value);
+
+ if (!(hasValue ^ StringUtils.hasText(expression))) {
+ throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' is required on a gateway's header.");
+ }
+ headerExpressions.put(name, hasValue ? new LiteralExpression(value): PARSER.parseExpression(expression));
+ }
+ }
+
}
else if (methodMetadataMap != null && methodMetadataMap.size() > 0) {
GatewayMethodMetadata methodMetadata = methodMetadataMap.get(method.getName());
if (methodMetadata != null) {
- payloadExpression = methodMetadata.getPayloadExpression();
- headerExpressions = methodMetadata.getHeaderExpressions();
+ if (StringUtils.hasText(methodMetadata.getPayloadExpression())) {
+ payloadExpression = methodMetadata.getPayloadExpression();
+ }
+ if (!CollectionUtils.isEmpty(methodMetadata.getHeaderExpressions())) {
+ headerExpressions.putAll(methodMetadata.getHeaderExpressions());
+ }
String requestChannelName = methodMetadata.getRequestChannelName();
if (StringUtils.hasText(requestChannelName)) {
requestChannel = this.resolveChannelName(requestChannelName);
2  ...ntegration-core/src/main/resources/org/springframework/integration/config/xml/spring-integration-4.0.xsd
View
@@ -651,7 +651,7 @@
<xsd:documentation>
<![CDATA[
An expression that will be used to generate the payload for all methods in the service interface
- unless explicitly overriden by a method declaration. Variables include #args, #methodName, #methodString
+ unless explicitly overridden by a method declaration. Variables include #args, #methodName, #methodString
and #methodObject; a bean resolver is also available, enabling expressions like "@someBean(#args)".
]]>
</xsd:documentation>
39 ...integration-core/src/test/java/org/springframework/integration/configuration/EnableIntegrationTests.java
View
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.hamcrest.Matchers;
@@ -33,10 +34,15 @@
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.integration.annotation.Gateway;
+import org.springframework.integration.annotation.GatewayHeader;
+import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.MessageEndpoint;
+import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.annotation.Payload;
import org.springframework.integration.annotation.Publisher;
import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.annotation.Transformer;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
@@ -73,6 +79,9 @@
@Autowired
private MessageHistoryConfigurer configurer;
+ @Autowired
+ private TestGateway testGateway;
+
@Test
public void testAnnotatedServiceActivator() {
this.input.send(MessageBuilder.withPayload("Foo").build());
@@ -112,8 +121,15 @@ public void testChangePatterns() {
assertEquals("*", TestUtils.getPropertyValue(this.configurer, "componentNamePatterns", String[].class)[0]);
}
+ @Test
+ public void testMessagingGateway() {
+ String payload = "bar";
+ assertEquals(payload.toUpperCase(), this.testGateway.echo(payload));
+ }
+
@Configuration
- @ComponentScan(basePackageClasses = EnableIntegrationTests.class)
+ @ComponentScan
+ @IntegrationComponentScan
@EnableIntegration
@PropertySource("classpath:org/springframework/integration/configuration/EnableIntegrationTests.properties")
@EnableMessageHistory({"input", "publishedChannel"})
@@ -147,6 +163,11 @@ public PollableChannel publishedChannel() {
return new QueueChannel();
}
+ @Bean
+ public DirectChannel gatewayChannel() {
+ return new DirectChannel();
+ }
+
}
@MessageEndpoint
@@ -159,6 +180,22 @@ public String handle(String payload) {
return payload.toUpperCase();
}
+ @Transformer(inputChannel = "gatewayChannel")
+ public String transform(Message<String> message) {
+ assertTrue(message.getHeaders().containsKey("foo"));
+ assertEquals("FOO", message.getHeaders().get("foo"));
+ assertTrue(message.getHeaders().containsKey("calledMethod"));
+ assertEquals("echo", message.getHeaders().get("calledMethod"));
+ return this.handle(message.getPayload());
+ }
+ }
+
+ @MessagingGateway(defaultRequestChannel = "gatewayChannel", defaultHeaders = @GatewayHeader(name = "foo", value = "FOO"))
+ public static interface TestGateway {
+
+ @Gateway(headers = @GatewayHeader(name = "calledMethod", expression="#gatewayMethod.name"))
+ String echo(String payload);
+
}
}
2  ...integration-core/src/test/java/org/springframework/integration/gateway/GatewayProxyFactoryBeanTests.java
View
@@ -336,7 +336,7 @@ public void testProgrammaticWiring() throws Exception {
gpfb.setServiceInterface(TestEchoService.class);
QueueChannel drc = new QueueChannel();
gpfb.setDefaultRequestChannel(drc);
- gpfb.setDefaultReplyTimeout(0);
+ gpfb.setDefaultReplyTimeout(0L);
GatewayMethodMetadata meta = new GatewayMethodMetadata();
meta.setHeaderExpressions(Collections. <String, Expression> singletonMap("foo", new LiteralExpression("bar")));
gpfb.setGlobalMethodMetadata(meta);
40 src/reference/docbook/gateway.xml
View
@@ -224,6 +224,46 @@ public String send2(Map foo, Map bar);
</para>
</section>
+ <section id="messaging-gateway-annotation">
+ <title>@MessagingGateway annotation</title>
+ <para>
+ Starting with <emphasis>version 4.0</emphasis> gateway service interfaces can be marked with
+ <code>@MessagingGateway</code> annotation to replace the <code>&lt;gateway&gt;</code> xml element configuration.
+ Just to compare:
+ </para>
+ <programlisting language="xml"><![CDATA[<int:gateway id="myGateway" service-interface="org.foo.bar.TestGateway"
+ default-request-channel="inputC">
+ <int:default-header name="calledMethod" expression="#gatewayMethod.name"/>
+ <int:method name="echo" request-channel="inputA" reply-timeout="2" request-timeout="200"/>
+ <int:method name="echoUpperCase" request-channel="inputB">
+ <int:header name="foo" value="bar"/>
+ </int:method>
+ <int:method name="echoViaDefault"/>
+</int:gateway>]]></programlisting>
+ <programlisting language="java"><![CDATA[
+@MessagingGateway(name = "myGateway", defaultRequestChannel = "inputC",
+ defaultHeaders = @GatewayHeader(name = "calledMethod",
+ expression="#gatewayMethod.name"))
+public interface TestGateway {
+
+ @Gateway(requestChannel = "inputA", replyTimeout = 2, requestTimeout = 200)
+ String echo(String payload);
+
+ @Gateway(requestChannel = "inputB, headers = @GatewayHeader(name = "foo", value="bar"))
+ String echoUpperCase(String payload);
+
+ String echoViaDefault(String payload);
+
+}]]></programlisting>
+ <para>
+ As far as it is an interface and Spring Integration produces the <code>proxy</code> for it
+ by its messaging infrastructure, there is no standard process to parse that annotation and
+ register <interfacename>BeanDefinition</interfacename> in the application context. So, be sure to use
+ the <code>@IntegrationComponentScan</code> annotation together with the <code>@Configuration</code> -
+ <xref linkend="enable-integration"/>.
+ </para>
+ </section>
+
<section id="gateway-calling-no-argument-methods">
<title>Invoking No-Argument Methods</title>
<para>
8 src/reference/docbook/overview.xml
View
@@ -19,7 +19,7 @@
enterprise applications. Developers benefit from the consistency of this model and especially the fact that it is
based upon well-established best practices such as programming to interfaces and favoring composition over
inheritance. Spring's simplified abstractions and powerful support libraries boost developer productivity while
- simultaneously increasing the level of testability and portability.
+ simultaneously increasing the level of testability and portability.
</para>
<para>
Spring Integration is motivated by these same goals and principles. It
@@ -344,6 +344,12 @@
components and 2 or more child contexts that do use Spring Integration. It would enable these common
components to be declared once only, in the parent context.
</para>
+ <para>
+ <code>@IntegrationComponentScan</code> annotation has been also introduced to permit classpath
+ scanning. This annotation plays similar role as standard Spring Framework <code>@ComponentScan</code> annotation,
+ but it is restricted just to Spring Integration specific components and annotations,
+ which aren't reachable by standard component scan mechanism. For example <xref linkend="messaging-gateway-annotation"/>.
+ </para>
</section>
</chapter>
54 src/reference/docbook/whats-new.xml
View
@@ -19,28 +19,20 @@
See <xref linkend="mqtt"/>
</para>
</section>
- </section>
-
- <section id="4.0-general">
- <title>General Changes</title>
- <section>
- <title>Requires Spring Framework 4.0</title>
- <para>
- Core messaging abstractions (<interfacename>Message</interfacename>,
- <interfacename>MessageChannel</interfacename> etc) have moved to the Spring
- Framework <code>spring-messaging</code> module. Users who reference these
- classes directly in their code will need to make changes as described in
- the first section of the
- <ulink url="https://github.com/spring-projects/spring-integration/wiki/Spring-Integration-3.0-to-4.0-Migration-Guide"
- >Migration Guide</ulink>.
- </para>
- </section>
<section id="4.0-enable-configuration">
<title>@EnableIntegration</title>
<para>
The <code>@EnableIntegration</code> annotation has been added, to permit declaration of
- standard Spring Integration beans when using <code>@Configuration</code> classes. See
- <xref linkend="enable-integration"/> for more information.
+ standard Spring Integration beans when using <code>@Configuration</code> classes.
+ See <xref linkend="enable-integration"/> for more information.
+ </para>
+ </section>
+ <section id="4.0-component-scan">
+ <title>@IntegrationComponentScan</title>
+ <para>
+ The <code>@IntegrationComponentScan</code> annotation has been added, to permit classpath
+ scanning for Spring Integration specific components.
+ See <xref linkend="enable-integration"/> for more information.
Artem Bilan Collaborator

Pay attention, please, here to the result link in the PDF: What is that [10] ?

Gary Russell Owner

It's the page number (my PDF viewer - linux) puts up a tool tip when you hover over the link "Go to page 10".

Artem Bilan Collaborator

Let it be, but it is interest why there is no page number with other links?..

Gary Russell Owner

It's because the linkend references a <para/> instead of a <section/>.

A section can run over multiple pages; I think it's more important to jump to the appropriate paragraph than worry about this little inconsistency.

Artem Bilan Collaborator

Sure! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
</para>
</section>
<section id="4.0-message-history">
@@ -51,6 +43,14 @@
by a JMX MBean. For more information, see <xref linkend="message-history"/>.
</para>
</section>
+ <section id="4.0-messaging-gateway">
+ <title>@MessagingGateway</title>
+ <para>
+ Messaging gateway interfaces can now be configured with the <code>@MessagingGateway</code> annotation.
+ It is an analogue of <code>&lt;gateway&gt;</code> xml element.
+ For more information, see <xref linkend="messaging-gateway-annotation"/>.
+ </para>
+ </section>
<section id="4.0-boot">
<title>Spring Boot @EnableAutoConfiguration</title>
<para>
@@ -60,7 +60,23 @@
<code>@EnableAutoConfiguration</code>.
For more information see
<ulink url="http://projects.spring.io/spring-boot/docs/spring-boot-autoconfigure/README.html"
- >Spring Boot - AutoConfigure</ulink>.
+ >Spring Boot - AutoConfigure</ulink>.
+ </para>
+ </section>
+ </section>
+
+ <section id="4.0-general">
+ <title>General Changes</title>
+ <section>
+ <title>Requires Spring Framework 4.0</title>
+ <para>
+ Core messaging abstractions (<interfacename>Message</interfacename>,
+ <interfacename>MessageChannel</interfacename> etc) have moved to the Spring
+ Framework <code>spring-messaging</code> module. Users who reference these
+ classes directly in their code will need to make changes as described in
+ the first section of the
+ <ulink url="https://github.com/spring-projects/spring-integration/wiki/Spring-Integration-3.0-to-4.0-Migration-Guide"
+ >Migration Guide</ulink>.
</para>
</section>
<section id="4.0-xpath-header-enricher-header-type">
Something went wrong with that request. Please try again.