Hammurapi Group |
Java tools and libraries |
|
JMS Adapter
JMS Adapter abstracts Java applications from JMS API's. It can be used by Java applications to
Using JMS Adapter Java application can act as service consumer and provider at the same time. Also JMS Adapter itself can be started as a standalone Java applicaition and act as a service orchestrator. The Hammurapi Group JMS Adapter is a true adapter because it adapts to the client, whether the client produces or consumes messages, instead of the client having to adopt adapter-provided API's.
Features
TutorialInstallation
How to start and stop service or listenerOn Windows start jms-adapter.bat
On *x systems ./jms-adapter.sh ./
Use listener.xml to start listener and service.xml to start service. To stop service or listener simply terminate JVM with Ctrl-C. Adapter's shutdown hook will properly stop the adapter. Step 1: ListenerListener adapter configuration is stored in listener.xml. JMS text message with XML payload is converted to a Java object by the simple xml converter. The java object then is processed by biz.hammurapi.jms.adapter.tutorial.SampleProcessor. Exceptions are also handled by this class. <ns:jms-adapter xmlns:ns="http://www.hammurapi.biz/jms/adapter/definition"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ns:name>Listener</ns:name> <ns:description>Sample service</ns:description> <ns:connection> <ns:name>active-mq</ns:name> <ns:description>Active MQ connection</ns:description> <ns:reuse-thread-session>true</ns:reuse-thread-session> <ns:listener> <ns:name>sample-listener</ns:name> <ns:description>Sample listener</ns:description> <ns:destination>requestQueue</ns:destination> <ns:queue-from-session>true</ns:queue-from-session> <ns:bind-type>xml-simple</ns:bind-type> <ns:processor type="biz.hammurapi.jms.adapter.tutorial.SampleProcessor"/> </ns:listener> <ns:factory type="org.apache.activemq.ActiveMQConnectionFactory"> <ns:property name="brokerURL">tcp://localhost:61616</ns:property> </ns:factory> </ns:connection> <ns:bind-type type="biz.hammurapi.jms.adapter.converters.SimpleXmlConverter"> <ns:name>xml-simple</ns:name> <ns:description>Simple XML converter</ns:description> </ns:bind-type> </ns:jms-adapter> Listing 1. listener.xml Listing 1 shows XML configuration of JMS listener. This is a very basic configuration with many elements omitted. Please consult jms-adapter.xsd in HGee distribution for the full list of supported elements and attributes. package biz.hammurapi.jms.adapter.tutorial;
import biz.hammurapi.jms.adapter.Processor; import biz.hammurapi.util.ExceptionSink; /** * Receives object and prints it to console */ public class SampleProcessor implements Processor, ExceptionSink { public Object process(Object obj) { System.out.println(obj); return null; } public void consume(Object source, Exception e) { e.printStackTrace(); } } Listing 2. SampleProcessor. Start listener and then send messages to the requestQueue from ActiveMQ JMX console. You will see that messages in plain text cause exceptions, XML messages with a single text element and no attributes get successfully processed. If you add attribute type="java.lang.Integer" you will observe an exception if element content is not a number. ExamplesHello!
Invalid payload - not XML. <greeting>Hello</greeting>
Valid payload - default type is String. <greeting type="java.lang.String">Hello!</greeting>
Valid payload - explicit type specification. <number>323</number>
Valid payload - String. <number type="java.lang.Integer">323</number>
Valid payload - Integer. <number type="java.lang.Integer">Hello!</number>
Invalid payload - Hello! cannot be parsed to java.lang.Integer. Stop listener when you are done with experiments. Step 2: Service and consumer
In this step we will start an adapter intance with a service which calculates string length. Then this services will be consumed by a Java application through an adapter instance created by the application. <?xml version="1.0" encoding="UTF-8"?>
<ns:jms-adapter
xmlns:ns="http://www.hammurapi.biz/jms/adapter/definition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ns:name>Service container</ns:name> <ns:description>Hosts sample service</ns:description> <ns:connection> <ns:name>active-mq</ns:name> <ns:description>Active MQ connection</ns:description> <ns:reuse-thread-session>true</ns:reuse-thread-session> <ns:listener> <ns:name>service</ns:name> <ns:description>String length service</ns:description> <ns:destination>requestQueue</ns:destination> <ns:reply-destination>replyQueue</ns:reply-destination> <ns:queue-from-session>true</ns:queue-from-session> <ns:bind-type>xml-simple</ns:bind-type> <ns:processor type="biz.hammurapi.jms.adapter.tutorial.ServiceProcessor"/> </ns:listener> <ns:factory type="org.apache.activemq.ActiveMQConnectionFactory"> <ns:property name="brokerURL">tcp://localhost:61616</ns:property> </ns:factory> </ns:connection> <ns:bind-type type="biz.hammurapi.jms.adapter.converters.SimpleXmlConverter"> <ns:name>xml-simple</ns:name> <ns:description>Simple XML converter</ns:description> </ns:bind-type> <ns:measurement-consumer type="biz.hammurapi.metrics.SlicingMeasurementConsumer"/> </ns:jms-adapter> Listing 3. service.xml package biz.hammurapi.jms.adapter.tutorial;
import biz.hammurapi.jms.adapter.Processor;
/**
* Receives object and prints it to console. Returns object length if * object is string. Throws exception otherwise. */ public class ServiceProcessor implements Processor { public Object process(Object obj) throws Exception {
System.out.println(obj); if (obj instanceof String) { return new Integer(((String) obj).length()); } else { throw new IllegalArgumentException(obj==null ? "Request is null" : "Unexpected request type: "+obj.getClass()); } } }
Listing 4. ServiceProcessor.java <?xml version="1.0" encoding="UTF-8"?>
<ns:jms-adapter
xmlns:ns="http://www.hammurapi.biz/jms/adapter/definition" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ns:name>Service consumer</ns:name> <ns:description>Provides access to the sample service</ns:description> <ns:connection> <ns:name>active-mq</ns:name> <ns:description>Active MQ connection</ns:description> <ns:reuse-thread-session>true</ns:reuse-thread-session> <ns:service> <ns:name>string-length</ns:name> <ns:description>Invokes the string length service</ns:description> <ns:request-destination>requestQueue</ns:request-destination> <ns:reply-destination>replyQueue</ns:reply-destination> <ns:queue-from-session>true</ns:queue-from-session> <ns:bind-type>xml-simple</ns:bind-type> </ns:service> <ns:proxy-service> <ns:name>proxy-string-length</ns:name> <ns:description>Invokes string length service through interface.</ns:description> <ns:interface>biz.hammurapi.jms.adapter.tutorial.StringLengthService</ns:interface> <ns:alias>proxy</ns:alias> <ns:method> <ns:name>length</ns:name> <ns:service> <ns:name>Invocation service</ns:name> <ns:request-destination>requestQueue</ns:request-destination> <ns:queue-from-session>true</ns:queue-from-session> <ns:timeout>3000</ns:timeout> <ns:bind-type>xml-simple</ns:bind-type> <ns:property name="to-xml-style">file:invocation.xsl</ns:property> </ns:service> </ns:method> </ns:proxy-service> <ns:factory type="org.apache.activemq.ActiveMQConnectionFactory"> <ns:property name="brokerURL">tcp://localhost:61616</ns:property> </ns:factory> </ns:connection> <ns:bind-type type="biz.hammurapi.jms.adapter.converters.SimpleXmlConverter"> <ns:name>xml-simple</ns:name> <ns:description>Simple XML converter</ns:description> </ns:bind-type> </ns:jms-adapter> Listing 5. service-consumer.xml service-consumer.xml contains definitions of two types of services - a simple Service and a Proxy Service. package biz.hammurapi.jms.adapter.tutorial;
import java.io.File;
import org.apache.xmlbeans.XmlObject;
import biz.hammurapi.jms.adapter.JmsAdapter;
import biz.hammurapi.jms.adapter.JmsService; import biz.hammurapi.jms.adapter.definition.JmsAdapterDocument; public class ServiceConsumer { /**
* @param args */ public static void main(String[] args) throws Exception { XmlObject document = XmlObject.Factory.parse(new File("service-consumer.xml")); if (document instanceof JmsAdapterDocument) { biz.hammurapi.jms.adapter.definition.JmsAdapter definition = ((JmsAdapterDocument) document).getJmsAdapter(); JmsAdapter adapter = new JmsAdapter(definition); adapter.start(); try { JmsService service = (JmsService) adapter.get("connections/active-mq/services/string-length"); System.out.println(service.request("Hello")); //System.out.println(service.request(new Integer(385))); } finally { adapter.stop(); } } else { System.err.println("Invalid adapter definition."); } } }
Listing 6. ServiceConsumer.java Listing 6 shows to how to invoke our string length service through the simple service. Proxy services use the same bind types as regular services. In order to do this method invocations are wrapped into instances of Invocation. I our example we use XML payload. Therefore Invocation instance is converted to XML as shown below. <object method-name="length" type="biz.hammurapi.jms.adapter.Invocation">
<state type="java.util.HashMap"> <entry> <key type="java.lang.String">Value</key> <value type="java.lang.String">Hello</value> </entry> </state> </object> Listing 7. Invocation. Our service cannot accept such payload and we have to transform invocation XML to a single string XML. We achieve this by applying a simple stylesheet to the invocation before writing it to a JMS message. Listing 8 shows the stylesheet. Stylesheet name is specified in "to-xml-style" property in Listing 5. <?xml version="1.0" encoding="UTF-8"?>
<xs:stylesheet xmlns:xs="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xs:output method="xml"/> <xs:template match="/"> <xs:for-each select="object/state/entry/value"> <value type="{@type}"><xs:value-of select="text()"/></value> </xs:for-each> </xs:template> </xs:stylesheet> Listing 8. Invocation stylesheet. package biz.hammurapi.jms.adapter.tutorial;
import java.io.File;
import org.apache.xmlbeans.XmlObject;
import biz.hammurapi.jms.adapter.JmsAdapter;
import biz.hammurapi.jms.adapter.ProxyService; import biz.hammurapi.jms.adapter.definition.JmsAdapterDocument; public class ProxyServiceConsumer { /**
* @param args */ public static void main(String[] args) throws Exception { XmlObject document = XmlObject.Factory.parse(new File("service-consumer.xml")); if (document instanceof JmsAdapterDocument) { biz.hammurapi.jms.adapter.definition.JmsAdapter definition = ((JmsAdapterDocument) document).getJmsAdapter(); JmsAdapter adapter = new JmsAdapter(definition); adapter.start(); try { StringLengthService proxy = (StringLengthService) adapter.get("proxy"); proxy.setValue("Hello"); System.out.println(proxy.getValue()); System.out.println(proxy.length()); } finally { adapter.stop(); } } else { System.err.println("Invalid adapter definition."); } } }
Listing 8. ProxyServiceConsumer.java Listing 8 shows how to invoke the string length service through a proxy. Note that string to be sent to the service is stored in locally by invoking setValue() method and then it is shipped to the service as part of invocation payload. It is worth mentioning how JMS Adapter implements exception propagation. The adapter tries to reproduce the remote exception. E.g. if our service throws IllegalArgumentException with a message "Unexpected argument of type java.lang.Integer", the service will instantiate the same exception type with the same message when it receives error notification message. Stack trace is not propagated. In the case of proxy services it is possible to define a wrapper exception to "wrap" remote exceptions not listed in interface method throws clause. This allows to avoid UnexpectedThrowableException. Runtime exceptions and declared exceptions are not wrapped. JCA supportJMS Adapter implements JCA 1.5 Resource Adapter. The distribution package contains ra.xml file. Package it with needed jars into a .rar file and deploy to the application server. The resource adapter wraps JMS Adapter instance. There are two connection modes - stateless and stateful. In the stateful mode services retrieved from the adapter are cached in connection. In the case of proxy services adapter returns proxy instances. If proxy instances maintains state by using local delegates, then this state becomes associated with the connection. When JMS Adapter is deployed as JCA Resource adapter, default worker uses WorkManager to execute asynchronous tasks. JMS Adapter implements activation specification and can deliver invocations to message driven beans which implement any Java interface. CookbookMetrics collectionAll services collect metrics such as processing time, number of invocations, number of exceptions. Individual measurements are passed to the measurement consumer, if it is present. In the Listing 3 you can see a very simple configuration of measurement consumer. This configuration aggregates measurements over 60 seconds and prints aggregated values to console. The Common and Enterprise Extensions libraries contain a number of implementations of measurement collectors for more advanced metrics collection. On a service bus with many servcies it would be quite natural have a metrics collecting topic and metrics collecting service(s). In this case adapter configuration shall contain a difinition of such a service. Measurement consumer would lookup the service using the naming bus and send aggregated metrics to the bus through that service. Metrics can be used for service monitoring and alerting. For example if level of errors or processing time on some service goes above some level another service can automatically generate alerts. You can read more about the metrics collection framework here Metrics framework. If metrics are collected with consumer code "aspect" they can be used for usage-base billing of consumers in service buses where service consumers have to pay for accessing services. OrchestractionIn the adapter all components can reference each other through the naming bus. Therefore development of orchestrating services is straightforward:
CachingTransparent caching can be implemted by using Proxy Services with caching Local delegates. You can also implement a Generic Service for caching which caching Local delegates will use. Rules based XML processingRules engine, such as Hammurapi Rules, XmlBeans, biz.hammurapi.util.BeanVisitable and JMS Adapter can be combined together to implement rules-based validation, enrichment and transformation (e.g. element-level encryption/decryption) of messages with XML payload. Augmented with XSL transformation of incoming messages this combination can be used as an "edge-service" receiving messages from external clients and converting them to internal "canonical" format. This is how it can be done:
ScenarioYour organization provides different types of lending services. It works with car dealerships, mortgage brokers, furniture stores, etc. Partners collect credit applications from their customers a submit electronically to your organization. Different partners use different data formats. Also different products require different data, e.g. mortgage application is different from car loan application. Nonetheless they have a lot in common, e.g. Address or Person descriptions can be shared between different application types. In order to optimize application processing you can do the following:
biz.hammurapi.util.BeanVisitable will pass each XML type in an XML document being visited to rules. It wouldn't matter at what part of a credit application document Address type is encountered and how many addresses the application contains - each instance of Address XML type will be passed to rules working on Address to be validated and enriched. Based on validation results incoming documents can be published to the internal bus for further automated processing, returned to the sender, or be sent to manual processing. Internal services will consume validated credit applications in canonical format and as such will be able to concentrate on the service logic instead of validation and format transformation. Database accessHGee library contains BasicDataSourceComponent class, which extends Apache Commons DBCP's BasicDataSource. This class can be used as Generic Service in JMS Adapter. Working with adapter XML definitions programmaticallyHGee distribution contains jms-adapter.jar, which is jms-adapter.xsd compiled with XmlBeans. You can use it to programmatically construct adapter definitions from other sources, e.g. database tables. RoadmapWe expect the adapter to grow organically through development of Bind Type implementations by the user community and by the core team on as needed basis. Other areas of improvement of the JMS Adapter include:
GlossaryClassLLocal delegateLocal delegate maintains proxy object state. Local delegate performs proxy invocations not mapped to JMS services. Remote invocations are also filtered through the local delegate so it can modify arguments, return value or prevent remote invocation at all. For example local delegate can cache values returned by remote invocations. Local delegates must implement biz.hammurapi.jms.adapter.LocalDelegate interface.SServiceComponentBBind TypeBind types are components which convert JMS messages to Java objects and vice versa. These components shall implement biz.hammurapi.jms.adapter.Converter interface.CContextJNDI context. Contains JNDI connections.FFactory ConnectionJMS Connection which is created using vendor-specific API's. Both Factory Connection and JNDI Connection implement connection pooling, session pooling, and automatic re-connnection on failure. Also these components can be configured to refresh underlying JMS connections on a regular interval.GGeneric ServiceNon-JMS service. Generic service can act as a helper component for JMS services or as a consumer/coordinator of JMS services.JJMS AdapterContainer of other JMS components. It provides naming bus and lifecycle management services for its children. Adapter is configured from an XML document (definition). The definition shall conform to http://www.hammurapi.biz/jms/adapter/definition XML schema defined in jmd-adapter.xsd file in HGee distribution. The schema file contains detailed description of each XML type and element.JMS ServiceJMS Service is a component capable of sending JMS messages. It can send messages in fire-and-forget and request-reply modes. Sending messages can be done synchronously (in the current thread), or asynchronously (by delegating to worker). Reply can be received by the caller as method return value or through a callback interface instance.JNDI ConnectionJMS Connection, which connection factory is obtained through JNDI lookup. Both Factory Connection and JNDI Connection implement connection pooling, session pooling, and automatic re-connnection on failure. Also these components can be configured to refresh underlying JMS connections on a regular interval.LListenerJMS listener. This component uses Bind Type components to convert messages to Java objects. Java objects are then passed to processor instance for further processing.MMeasurement ConsumerThis component collects metrics reported by other components.MethodSpecifies how interface method invocation shall be translated to JMS request/reply or fire-and-forget (if method is void or return value is of no interest for the caller). Methods can be configured to be executed asynchronously and store return value to local proxy state (pre-fetch).PProxy ServiceProxy service maps Java interface method calls to JMS request/reply message exchange. Proxy instances created by proxy service maintain local state and local behavior through Local delegate. Local delegate also acts as a filter for remote calls. In other words, proxy instance is an object with local state and distributed behavior. With proxy services remote services on the service bus can be stateless but appear to be stateful for consumers. A proxy service can implement any Java interface or collection of interfaces. This feature allows almost transparent migration to message-based communications from other remoting technologies such as RMI. WWorkerWorkers, typically implemented as thread pools, are used for delegation of work from other components for asynchrhonous execution. |