The (Aspect-oriented Programming) AOP message publishing feature lets you construct and send a message as a by-product of a method invocation. For example, imagine you have a component and, every time the state of this component changes, you want to be notified by a message. The easiest way to send such notifications is to send a message to a dedicated channel, but how would you connect the method invocation that changes the state of the object to a message sending process, and how should the notification message be structured? The AOP message publishing feature handles these responsibilities with a configuration-driven approach.
Spring Integration provides two approaches: XML configuration and annotation-driven (Java) configuration.
The annotation-driven approach lets you annotate any method with the @Publisher
annotation to specify a channel attribute.
Starting with version 5.1, to switch this functionality on, you must use the @EnablePublisher
annotation on some @Configuration
class.
See Section 5.5, “Configuration and @EnableIntegration
” for more information.
The message is constructed from the return value of the method invocation and sent to the channel specified by the channel attribute.
To further manage message structure, you can also use a combination of both @Payload
and @Header
annotations.
Internally, this message publishing feature of Spring Integration uses both Spring AOP by defining PublisherAnnotationAdvisor
and the Spring Expression Language (SpEL), giving you considerable flexibility and control over the structure of the Message
it publishes.
The PublisherAnnotationAdvisor
defines and binds the following variables:
#return
: Binds to a return value, letting you reference it or its attributes (for example, #return.something
, where something is an attribute of the object bound to #return
)
#exception
: Binds to an exception if one is thrown by the method invocation
#args
: Binds to method arguments so that you can extract individual arguments by name (for example, #args.fname
)
Consider the following example:
@Publisher public String defaultPayload(String fname, String lname) { return fname + " " + lname; }
In the preceding example, the message is constructed with the following structure:
The following example is the same as the preceding example, except that it does not use a default publishing channel:
@Publisher(channel="testChannel") public String defaultPayload(String fname, @Header("last") String lname) { return fname + " " + lname; }
Instead of using a default publishing channel, we specify the publishing channel by setting the channel attribute of the @Publisher
annotation.
We also add a @Header
annotation, which results in the message header named last having the same value as the lname method parameter.
That header is added to the newly constructed message.
The following example is almost identical to the preceding example:
@Publisher(channel="testChannel") @Payload public String defaultPayloadButExplicitAnnotation(String fname, @Header String lname) { return fname + " " + lname; }
The only difference is that we use a @Payload
annotation on the method to explicitly specify that the return value of the method should be used as the payload of the message.
The following example expands on the previous configuration by using the Spring Expression Language in the @Payload
annotation to further instruct the framework about how the message should be constructed:
@Publisher(channel="testChannel") @Payload("#return + #args.lname") public String setName(String fname, String lname, @Header("x") int num) { return fname + " " + lname; }
In the preceding example, the message is a concatenation of the return value of the method invocation and the lname input argument. The Message header named x has its value determined by the num input argument. That header is added to the newly constructed message.
@Publisher(channel="testChannel") public String argumentAsPayload(@Payload String fname, @Header String lname) { return fname + " " + lname; }
In the preceding example, you see another usage of the @Payload
annotation.
Here, we annotate a method argument that becomes the payload of the newly constructed message.
As with most other annotation-driven features in Spring, you need to register a post-processor (PublisherAnnotationBeanPostProcessor
).
The following example shows how to do so:
<bean class="org.springframework.integration.aop.PublisherAnnotationBeanPostProcessor"/>
For a more concise configuration, you can instead use namespace support, as the following example shows:
<int:annotation-config> <int:enable-publisher default-publisher-channel="defaultChannel"/> </int:annotation-config>
For Java configuration, you must use the @EnablePublisher
annotation, as the following example shows:
@Configuration @EnableIntegration @EnablePublisher("defaultChannel") public class IntegrationConfiguration { ... }
Similar to other Spring annotations (@Component
, @Scheduled
, and so on), you can also use @Publisher
as a meta-annotation.
This means that you can define your own annotations that are treated in the same way as the @Publisher
itself.
The following example shows how to do so:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Publisher(channel="auditChannel") public @interface Audit { ... }
In the preceding example, we define the @Audit
annotation, which is itself annotated with @Publisher
.
Also note that you can define a channel
attribute on the meta-annotation to encapsulate where messages are sent inside of this annotation.
Now you can annotate any method with the @Audit
annotation, as the following example shows:
@Audit public String test() { return "Hello"; }
In the preceding example, every invocation of the test()
method results in a message with a payload created from its return value.
Each message is sent to the channel named auditChannel
.
One of the benefits of this technique is that you can avoid the duplication of the same channel name across multiple annotations.
You also can provide a level of indirection between your own, potentially domain-specific, annotations and those provided by the framework.
You can also annotate the class, which lets you apply the properties of this annotation on every public method of that class, as the following example shows:
@Audit static class BankingOperationsImpl implements BankingOperations { public String debit(String amount) { . . . } public String credit(String amount) { . . . } }
The XML-based approach lets you configure the same AOP-based message publishing functionality as a namespace-based configuration of a MessagePublishingInterceptor
.
It certainly has some benefits over the annotation-driven approach, since it lets you use AOP pointcut expressions, thus possibly intercepting multiple methods at once or intercepting and publishing methods to which you do not have the source code.
To configure message publishing with XML, you need only do the following two things:
MessagePublishingInterceptor
by using the <publishing-interceptor>
XML element.
MessagePublishingInterceptor
to managed objects.
The following example shows how to configure a publishing-interceptor
element:
<aop:config> <aop:advisor advice-ref="interceptor" pointcut="bean(testBean)" /> </aop:config> <publishing-interceptor id="interceptor" default-channel="defaultChannel"> <method pattern="echo" payload="'Echoing: ' + #return" channel="echoChannel"> <header name="things" value="something"/> </method> <method pattern="repl*" payload="'Echoing: ' + #return" channel="echoChannel"> <header name="things" expression="'something'.toUpperCase()"/> </method> <method pattern="echoDef*" payload="#return"/> </publishing-interceptor>
The <publishing-interceptor>
configuration looks rather similar to the annotation-based approach, and it also uses the power of the Spring Expression Language.
In the preceding example, the execution of the echo
method of a testBean
renders a Message
with the following structure:
Message
payload is of type String
with the following content: Echoing: [value]
, where value
is the value returned by an executed method.
Message
has a header with a name of things
and a value of something
.
Message
is sent to echoChannel
.
The second method is very similar to the first.
Here, every method that begins with repl renders a Message
with the following structure:
Message
payload is the same as in the preceding sample.
Message
has a header named things
whose value is the result of the SpEL expression 'something'.toUpperCase()
.
Message
is sent to echoChannel
.
The second method, mapping the execution of any method that begins with echoDef
, produces a Message
with the following structure:
Message
payload is the value returned by an executed method.
channel
attribute is not provided, the Message
is sent to the defaultChannel
defined by the publisher
.
For simple mapping rules you can rely on the publisher
defaults, as the following example shows:
<publishing-interceptor id="anotherInterceptor"/>
The preceding example maps the return value of every method that matches the pointcut expression to a payload and is sent to a default-channel
.
If you do not specify the defaultChannel
(as the preceding example does not do), the messages are sent to the global nullChannel
(the equivalent of /dev/null
).
Publishing occurs in the same thread as your component’s execution. So, by default, it is synchronous. This means that the entire message flow has to wait until the publisher’s flow completes. However, developers often want the complete opposite: to use this message-publishing feature to initiate asynchronous flows. For example, you might host a service (HTTP, WS, and so on) which receives a remote request. You may want to send this request internally into a process that might take a while. However you may also want to reply to the user right away. So, instead of sending inbound requests for processing to the output channel (the conventional way), you can use output-channel or a replyChannel header to send a simple acknowledgment-like reply back to the caller while using the message-publisher feature to initiate a complex flow.
The service in the following example receives a complex payload (which needs to be sent further for processing), but it also needs to reply to the caller with a simple acknowledgment:
public String echo(Object complexPayload) { return "ACK"; }
So, instead of hooking up the complex flow to the output channel, we use the message-publishing feature instead.
We configure it to create a new message, by using the input argument of the service method (shown in the preceding example), and send that to the localProcessChannel.
To make sure this flow is asynchronous, all we need to do is send it to any type of asynchronous channel (ExecutorChannel
in the next example).
The following example shows how to an asynchronous publishing-interceptor
:
<int:service-activator input-channel="inputChannel" output-channel="outputChannel" ref="sampleservice"/> <bean id="sampleservice" class="test.SampleService"/> <aop:config> <aop:advisor advice-ref="interceptor" pointcut="bean(sampleservice)" /> </aop:config> <int:publishing-interceptor id="interceptor" > <int:method pattern="echo" payload="#args[0]" channel="localProcessChannel"> <int:header name="sample_header" expression="'some sample value'"/> </int:method> </int:publishing-interceptor> <int:channel id="localProcessChannel"> <int:dispatcher task-executor="executor"/> </int:channel> <task:executor id="executor" pool-size="5"/>
Another way of handling this type of scenario is with a wire-tap. See the section called “Wire Tap”.
In the preceding sections, we looked at the message-publishing feature, which constructs and publishes messages as by-products of method invocations.
However, in those cases, you are still responsible for invoking the method.
Spring Integration 2.0 added support for scheduled message producers and publishers with the new expression
attribute on the inbound-channel-adapter element.
You can schedul based on several triggers, any one of which can be configured on the poller element.
Currently, we support cron
, fixed-rate
, fixed-delay
and any custom trigger implemented by you and referenced by the trigger attribute value.
As mentioned earlier, support for scheduled producers and publishers is provided via the <inbound-channel-adapter>
XML element.
Consider the following example:
<int:inbound-channel-adapter id="fixedDelayProducer" expression="'fixedDelayTest'" channel="fixedDelayChannel"> <int:poller fixed-delay="1000"/> </int:inbound-channel-adapter>
The preceding example creates an inbound channel adapter that constructs a Message
, with its payload being the result of the expression defined in the expression
attribute.
Such messages are created and sent every time the delay specified by the fixed-delay
attribute occurs.
The following example is similar to the preceding example, except that it uses the fixed-rate
attribute:
<int:inbound-channel-adapter id="fixedRateProducer" expression="'fixedRateTest'" channel="fixedRateChannel"> <int:poller fixed-rate="1000"/> </int:inbound-channel-adapter>
The fixed-rate
attribute lets you send messages at a fixed rate (measuring from the start time of each task).
The following example shows how you can apply a Cron trigger with a value specified in the cron
attribute:
<int:inbound-channel-adapter id="cronProducer" expression="'cronTest'" channel="cronChannel"> <int:poller cron="7 6 5 4 3 ?"/> </int:inbound-channel-adapter>
The following example shows how to insert additional headers into the message:
<int:inbound-channel-adapter id="headerExpressionsProducer" expression="'headerExpressionsTest'" channel="headerExpressionsChannel" auto-startup="false"> <int:poller fixed-delay="5000"/> <int:header name="foo" expression="6 * 7"/> <int:header name="bar" value="x"/> </int:inbound-channel-adapter>
The additional message headers can take scalar values or the results of evaluating Spring expressions.
If you need to implement your own custom trigger, you can use the trigger
attribute to provide a reference to any spring configured bean that implements the org.springframework.scheduling.Trigger
interface.
The following example shows how to do so:
<int:inbound-channel-adapter id="triggerRefProducer" expression="'triggerRefTest'" channel="triggerRefChannel"> <int:poller trigger="customTrigger"/> </int:inbound-channel-adapter> <beans:bean id="customTrigger" class="o.s.scheduling.support.PeriodicTrigger"> <beans:constructor-arg value="9999"/> </beans:bean>