さて、ServiceMix v2 から v3へで紹介したように、文書管理システムのβ版では、α版の積み残し項目を実装しました。具体的には次の点になります。
このうち1と3についてはServiceMix自体の問題もしくは設定の問題なので、サービス実装方法に関連する2,4,5について説明します。
今回の文書管理システムはPDFのデータを扱う必要があり、ServiceMix v2 から v3へで紹介したように、β版ではデータをメッセージに添付するようにしました。
ServiceMixではJBI規格に基づいて、サービス間でやりとりされるメッセージ(NormalizedMessage)には任意のバイナリデータを添付することができます。ServiceMixのHTTPコンポーネントは、受信したSOAPメッセージをNormalizedMessageに変換する際に、添付されたデータがあれは自動的にNormalizedMessageに添付しますので、サービスの実装側はそれを取り出せばよいということになります。ちなみにNormalizedMessageには、添付に関して次のようなメソッドが用意されています。
public void addAttachment(String id, DataHandler content) メッセージに、idという名前でcontentで示されるデータを添付する public DataHandler getAttachment(String id) メッセージからidという名前の添付データを取り出す public void removeAttachement(String id) メッセージからidという名前の添付データを削除する public Set getAttachementNames() メッセージに含まれる添付データの名前のリストを取得する
実際には、添付データはInputStream型で扱った方が楽なので、次のようなコードを追加してあります。
public InputStream getAttachment(String id) throws IOException { // in は NormalizedMessage DataHandler dh = in.getAttachment(id); if (dh == null) { return null; } else { return dh.getInputStream(); } }
ビジネスロジック側で発生した例外は、アダプタクラスで捕捉し、fail()メソッドを呼び出します。
public void onMessageExchange(MessageExchange exchange) throws MessagingException { ... try { ...ビジネスロジックの呼び出し... ...リプライメッセージの送信... } catch (TransformerException e) { fail(exchange, e); } catch (...) { fail(exchange, e); } }
failメソッドはアダプタクラスの親クラスであるPojoSupportクラスで定義されていて、エラーが発生したことを示すFaultメッセージを返信します。
ただしこのfailメソッドでは、ネストした例外の情報が伝わらないので、実際には次のようなコードを用いて独自にFaultメッセージを作って送信するようにしてあります。
public void fail(MessageExchange exchange, Exception e) throws MessagingException { try { // Faultメッセージを生成 Fault out = exchange.createFault(); // メッセージに例外の詳細情報をセット Document doc = DOMUtil.newDocument(); DOMUtil.addException(doc, e); out.setContent(DOMUtil.convertToSource(doc)); // エラーの送信 super.fail(exchange, out); } catch(MessagingException me) { super.fail(exchange, me); } catch(ParserConfigurationException pe) { super.fail(exchange, pe); } }
メッセージに例外の詳細情報を設定するコードは次の通りです。
public static void addException(Document doc, Exception e) { Element root = doc.createElement("Exception"); doc.appendChild(root); Element clazzes = doc.createElement("Classes"); root.appendChild(clazzes); // 再帰的にクラス名をDOMに追加する addCause(doc, clazzes, e); Element trace = doc.createElement("StackTrace"); StringWriter swriter = new StringWriter(); PrintWriter pwriter = new PrintWriter(swriter); e.printStackTrace(pwriter); trace.appendChild(doc.createTextNode(swriter.toString())); root.appendChild(trace); } private static void addCause(Document doc, Element parent, Throwable e) { Element clazz = doc.createElement("Class"); clazz.appendChild(doc.createTextNode(e.getClass().toString())); parent.appendChild(clazz); Throwable thr = e.getCause(); if (thr != null) { addCause(doc, parent, thr); } }
以下に、例外が発生した時にクライアント側に送られてくるSOAPのfaultメッセージの例を示します (見やすいように整形してあります)。
<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Body> <soapenv:Fault> <faultcode>soapenv:Server</faultcode> <faultstring/> <detail> <Exception> <Classes> <Class>class java.lang.reflect.InvocationTargetException</Class> <Class>class java.lang.Exception</Class> </Classes> <StackTrace> java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) ... </StackTrace> </Exception> </detail> </soapenv:Fault> </soapenv:Body> </soapenv:Envelope>
α版の実装では、ビジネスロジックをアダプタクラスを用いてサービス化していたのですが、実行されるのは常にcallというメソッドでした。
public interface BusinessLogic { public Document call(Document doc); }
サービスは一般に複数の機能を持つので、このメソッドの中でどの機能が呼ばれたかを判別して適切な処理にディスパッチする必要がありました。
このような処理はどのサービスでも必要なので、共通化してしまった方が楽ですし、間違いも少なくなります。そこでβ版ではサービス実装方法(1)で説明したアダプタクラスを拡張し、次のようなロジックを組み込みました。
サンプルとして実装したコードの一部を示します。アダプタクラスでは、メッセージが届いた時にonMessageExchangeメソッドが呼び出されるので、そこでオペレーションを調べて適切なメソッドを呼び出します。
public class LogicAdaptor extends ComponentSupport implements MessageExchangeListener { ... public void onMessageExchange(MessageExchange exchange) throws MessagingException... { ... try { // MessageExchange からオペレーション名を取得 QName qname = exchange.getOperation(); String operationName = null; if (qname != null) { operationName = qname.getLocalPart(); } if (operationName == null) { // メッセージ本体からオペレーション名を取得 Document doc = param.getDocument(); Element root = doc.getDocumentElement(); if (root == null) { throw new NoSuchMethodException( "No such method, tagname is null."); } else { // ルート要素名をオペレーション名とする operationName = root.getTagName(); } } ... // operationNameで指定されるメソッドを呼び出す Document result = BusinessLogicUtil.call(logic, operationName, param); ...