285 lines
14 KiB
XML
285 lines
14 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
|
<appendix id="samples">
|
|
|
|
<title>Spring Integration Samples</title>
|
|
|
|
<note>
|
|
Starting with the current release of Spring Integration the <emphasis>samples</emphasis> are distributed as independent
|
|
Maven-based projects (<ulink url="http://maven.apache.org/">http://maven.apache.org/</ulink>) to minimize the setup time.
|
|
Since each project is also an Eclipse-based project, they can be imported as is using "Import existing project wizard".
|
|
If you prefer another IDE, configuration should be very trivial, since a special Maven profile was setup to download all
|
|
of the required dependencies for all samples. Detailed instructions on how to build and run the samples are provided in
|
|
the <code>README.txt</code> file located in the <emphasis>samples</emphasis> directory of the main distribution.
|
|
</note>
|
|
|
|
<section id="samples-cafe">
|
|
<title>The Cafe Sample</title>
|
|
<para>
|
|
In this section, we will review a sample application that is included in the Spring Integration
|
|
distribution. This sample is inspired by one of the samples featured in Gregor Hohpe's
|
|
<ulink url="http://www.eaipatterns.com/ramblings.html">Ramblings</ulink>.
|
|
</para>
|
|
<para>
|
|
The domain is that of a Cafe, and the basic flow is depicted in the following diagram:
|
|
</para>
|
|
<para>
|
|
<mediaobject>
|
|
<imageobject>
|
|
<imagedata align="center" fileref="images/cafe-demo.png" format="PNG"/>
|
|
</imageobject>
|
|
</mediaobject>
|
|
</para>
|
|
<para>
|
|
The <classname>Order</classname> object may contain multiple <classname>OrderItems</classname>. Once the order
|
|
is placed, a <emphasis>Splitter</emphasis> will break the composite order message into a single message per
|
|
drink. Each of these is then processed by a <emphasis>Router</emphasis> that determines whether the drink is hot
|
|
or cold (checking the <classname>OrderItem</classname> object's 'isIced' property). The
|
|
<classname>Barista</classname> prepares each drink, but hot and cold drink preparation are handled by two
|
|
distinct methods: 'prepareHotDrink' and 'prepareColdDrink'. The prepared drinks are then sent to the Waiter where
|
|
they are aggregated into a <classname>Delivery</classname> object.
|
|
</para>
|
|
<para>
|
|
Here is the XML configuration:
|
|
<programlisting language="xml"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
|
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:beans="http://www.springframework.org/schema/beans"
|
|
xmlns:stream="http://www.springframework.org/schema/integration/stream"
|
|
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
|
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
|
|
http://www.springframework.org/schema/integration
|
|
http://www.springframework.org/schema/integration/spring-integration-1.0.xsd
|
|
http://www.springframework.org/schema/integration/stream
|
|
http://www.springframework.org/schema/integration/stream/spring-integration-stream-1.0.xsd">
|
|
|
|
<gateway id="cafe" service-interface="org.springframework.integration.samples.cafe.Cafe"/>
|
|
|
|
<channel id="orders"/>
|
|
<splitter input-channel="orders" ref="orderSplitter" method="split" output-channel="drinks"/>
|
|
|
|
<channel id="drinks"/>
|
|
<router input-channel="drinks" ref="drinkRouter" method="resolveOrderItemChannel"/>
|
|
|
|
<channel id="coldDrinks">
|
|
<queue capacity="10"/>
|
|
</channel>
|
|
<service-activator input-channel="coldDrinks" ref="barista"
|
|
method="prepareColdDrink" output-channel="preparedDrinks"/>
|
|
|
|
<channel id="hotDrinks">
|
|
<queue capacity="10"/>
|
|
</channel>
|
|
<service-activator input-channel="hotDrinks" ref="barista"
|
|
method="prepareHotDrink" output-channel="preparedDrinks"/>
|
|
|
|
<channel id="preparedDrinks"/>
|
|
<aggregator input-channel="preparedDrinks" ref="waiter"
|
|
method="prepareDelivery" output-channel="deliveries"/>
|
|
|
|
<stream:stdout-channel-adapter id="deliveries"/>
|
|
|
|
<beans:bean id="orderSplitter"
|
|
class="org.springframework.integration.samples.cafe.xml.OrderSplitter"/>
|
|
|
|
<beans:bean id="drinkRouter"
|
|
class="org.springframework.integration.samples.cafe.xml.DrinkRouter"/>
|
|
|
|
<beans:bean id="barista" class="org.springframework.integration.samples.cafe.xml.Barista"/>
|
|
|
|
<beans:bean id="waiter" class="org.springframework.integration.samples.cafe.xml.Waiter"/>
|
|
|
|
<poller id="poller" default="true">
|
|
<interval-trigger interval="1000"/>
|
|
</poller>
|
|
|
|
</beans:beans>]]></programlisting>
|
|
As you can see, each Message Endpoint is connected to input and/or output channels. Each endpoint will manage
|
|
its own Lifecycle (by default endpoints start automatically upon initialization - to prevent that add the
|
|
"auto-startup" attribute with a value of "false"). Most importantly, notice that the objects are simple POJOs
|
|
with strongly typed method arguments. For example, here is the Splitter:
|
|
<programlisting language="java"><![CDATA[public class OrderSplitter {
|
|
|
|
public List<OrderItem> split(Order order) {
|
|
return order.getItems();
|
|
}
|
|
}]]></programlisting>
|
|
In the case of the Router, the return value does not have to be a <interfacename>MessageChannel</interfacename>
|
|
instance (although it can be). As you see in this example, a String-value representing the channel name is
|
|
returned instead.
|
|
<programlisting language="java"><![CDATA[public class DrinkRouter {
|
|
|
|
public String resolveOrderItemChannel(OrderItem orderItem) {
|
|
return (orderItem.isIced()) ? "coldDrinks" : "hotDrinks";
|
|
}
|
|
}]]></programlisting>
|
|
</para>
|
|
<para>
|
|
Now turning back to the XML, you see that there are two <service-activator> elements. Each of these
|
|
is delegating to the same <classname>Barista</classname> instance but different methods: 'prepareHotDrink'
|
|
or 'prepareColdDrink' corresponding to the two channels where order items have been routed.
|
|
<programlisting language="java"><![CDATA[public class Barista {
|
|
|
|
private long hotDrinkDelay = 5000;
|
|
private long coldDrinkDelay = 1000;
|
|
|
|
private AtomicInteger hotDrinkCounter = new AtomicInteger();
|
|
private AtomicInteger coldDrinkCounter = new AtomicInteger();
|
|
|
|
public void setHotDrinkDelay(long hotDrinkDelay) {
|
|
this.hotDrinkDelay = hotDrinkDelay;
|
|
}
|
|
|
|
public void setColdDrinkDelay(long coldDrinkDelay) {
|
|
this.coldDrinkDelay = coldDrinkDelay;
|
|
}
|
|
|
|
public Drink prepareHotDrink(OrderItem orderItem) {
|
|
try {
|
|
Thread.sleep(this.hotDrinkDelay);
|
|
System.out.println(Thread.currentThread().getName()
|
|
+ " prepared hot drink #" + hotDrinkCounter.incrementAndGet()
|
|
+ " for order #" + orderItem.getOrder().getNumber() + ": " + orderItem);
|
|
return new Drink(orderItem.getOrder().getNumber(), orderItem.getDrinkType(),
|
|
orderItem.isIced(), orderItem.getShots());
|
|
}
|
|
catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public Drink prepareColdDrink(OrderItem orderItem) {
|
|
try {
|
|
Thread.sleep(this.coldDrinkDelay);
|
|
System.out.println(Thread.currentThread().getName()
|
|
+ " prepared cold drink #" + coldDrinkCounter.incrementAndGet()
|
|
+ " for order #" + orderItem.getOrder().getNumber() + ": " + orderItem);
|
|
return new Drink(orderItem.getOrder().getNumber(), orderItem.getDrinkType(),
|
|
orderItem.isIced(), orderItem.getShots());
|
|
}
|
|
catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
}]]></programlisting>
|
|
</para>
|
|
<para>
|
|
As you can see from the code excerpt above, the barista methods have different delays (the hot drinks take 5
|
|
times as long to prepare). This simulates work being completed at different rates. When the
|
|
<classname>CafeDemo</classname> 'main' method runs, it will loop 100 times sending a single hot drink and a
|
|
single cold drink each time. It actually sends the messages by invoking the 'placeOrder' method on the Cafe
|
|
interface. Above, you will see that the <gateway> element is specified in the configuration file. This
|
|
triggers the creation of a proxy that implements the given 'service-interface' and connects it to a channel.
|
|
The channel name is provided on the @Gateway annotation of the <interfacename>Cafe</interfacename> interface.
|
|
<programlisting language="java">public interface Cafe {
|
|
|
|
@Gateway(requestChannel="orders")
|
|
void placeOrder(Order order);
|
|
|
|
}</programlisting>
|
|
Finally, have a look at the <methodname>main()</methodname> method of the <classname>CafeDemo</classname> itself.
|
|
<programlisting language="java"><![CDATA[public static void main(String[] args) {
|
|
AbstractApplicationContext context = null;
|
|
if (args.length > 0) {
|
|
context = new FileSystemXmlApplicationContext(args);
|
|
}
|
|
else {
|
|
context = new ClassPathXmlApplicationContext("cafeDemo.xml", CafeDemo.class);
|
|
}
|
|
Cafe cafe = (Cafe) context.getBean("cafe");
|
|
for (int i = 1; i <= 100; i++) {
|
|
Order order = new Order(i);
|
|
order.addItem(DrinkType.LATTE, 2, false);
|
|
order.addItem(DrinkType.MOCHA, 3, true);
|
|
cafe.placeOrder(order);
|
|
}
|
|
}]]></programlisting>
|
|
</para>
|
|
<tip>
|
|
To run this sample as well as 8 others, refer to the <code>README.txt</code> within the "samples" directory
|
|
of the main distribution as described at the beginning of this chapter.
|
|
</tip>
|
|
<para>
|
|
When you run cafeDemo, you will see that the cold drinks are initially prepared more quickly than the hot drinks.
|
|
Because there is an aggregator, the cold drinks are effectively limited by the rate of the hot drink preparation.
|
|
This is to be expected based on their respective delays of 1000 and 5000 milliseconds. However, by configuring a
|
|
poller with a concurrent task executor, you can dramatically change the results. For example, you could use a
|
|
thread pool executor with 5 workers for the hot drink barista while keeping the cold drink barista as it is:
|
|
<programlisting language="xml"><![CDATA[<service-activator input-channel="hotDrinks"
|
|
ref="barista"
|
|
method="prepareHotDrink"
|
|
output-channel="preparedDrinks"/>
|
|
|
|
<service-activator input-channel="hotDrinks"
|
|
ref="barista"
|
|
method="prepareHotDrink"
|
|
output-channel="preparedDrinks">
|
|
]]><emphasis><![CDATA[<poller task-executor="pool">
|
|
<interval-trigger interval="1000"/>
|
|
</poller>]]></emphasis><![CDATA[
|
|
</service-activator>
|
|
|
|
]]><emphasis><![CDATA[<thread-pool-task-executor id="pool" core-size="5"/>]]></emphasis></programlisting>
|
|
</para>
|
|
<para>
|
|
Also, notice that the worker thread name is displayed with each invocation. You will see that the hot drinks are
|
|
prepared by the task-executor threads. If you provide a much shorter poller interval (such as 100 milliseconds),
|
|
then you will notice that occasionally it throttles the input by forcing the task-scheduler (the caller) to invoke
|
|
the operation.
|
|
</para>
|
|
<note>
|
|
In addition to experimenting with the poller's concurrency settings, you can also add the 'transactional'
|
|
sub-element and then refer to any PlatformTransactionManager instance within the context.
|
|
</note>
|
|
</section>
|
|
|
|
<section id="samples-xml-messaging">
|
|
<title>The xml messaging sample</title>
|
|
<para>
|
|
The xml messaging sample in the <package>org.springframework.integration.samples.xml</package> illustrates how to use
|
|
some of the provided components which deal with xml payloads. The sample uses the idea of processing an order for books
|
|
represented as xml.
|
|
</para>
|
|
<para>
|
|
First the order is split into a number of messages, each one representing a single order item using
|
|
the XPath splitter component.
|
|
<programlisting language="xml"><![CDATA[<si-xml:xpath-splitter id="orderItemSplitter" input-channel="ordersChannel"
|
|
output-channel="stockCheckerChannel" create-documents="true">
|
|
<si-xml:xpath-expression expression="/orderNs:order/orderNs:orderItem" namespace-map="orderNamespaceMap" />
|
|
</si-xml:xpath-splitter>
|
|
]]></programlisting>
|
|
</para>
|
|
<para>
|
|
A service activator is then used to pass the message into a stock checker POJO. The order item document is enriched with information
|
|
from the stock checker about order item stock level. This enriched order item message is then used to route the message. In the
|
|
case where the order item is in stock the message is routed to the warehouse. The XPath router makes use of a
|
|
<classname>MapBasedChannelResolver</classname> which maps the XPath evaluation result to a channel reference.
|
|
<programlisting language="xml"><![CDATA[<si-xml:xpath-router id="instockRouter" channel-resolver="mapChannelResolver"
|
|
input-channel="orderRoutingChannel" resolution-required="true">
|
|
<si-xml:xpath-expression expression="/orderNs:orderItem/@in-stock" namespace-map="orderNamespaceMap" />
|
|
</si-xml:xpath-router>
|
|
|
|
<bean id="mapChannelResolver"
|
|
class="org.springframework.integration.channel.MapBasedChannelResolver">
|
|
<property name="channelMap">
|
|
<map>
|
|
<entry key="true" value-ref="warehouseDispatchChannel" />
|
|
<entry key="false" value-ref="outOfStockChannel" />
|
|
</map>
|
|
</property>
|
|
</bean>
|
|
]]></programlisting>
|
|
</para>
|
|
<para>
|
|
Where the order item is not in stock the message is transformed using
|
|
xslt into a format suitable for sending to the supplier.
|
|
<programlisting language="xml"><![CDATA[<si-xml:xslt-transformer input-channel="outOfStockChannel" output-channel="resupplyOrderChannel"
|
|
xsl-resource="classpath:org/springframework/integration/samples/xml/bigBooksSupplierTransformer.xsl"/>
|
|
]]></programlisting>
|
|
</para>
|
|
</section>
|
|
|
|
</appendix> |