SOAP ヘッダーを追加することにより、WSDLに定義されたWebサービスをどのように拡張できるかを、この記事で説明をします。SOAPハンドラーがどのようにしてSOAPメッセージ・ヘッダーを作成して処理するか、そしてハンドラーをどのように適切に構成するかを示します。
まずは、図1に示されるJAX-RPCの呼び出しを考察することから始めます。
図1. JAX-RPC/SOAPメッセージ・フロー(メッセージの流れ)
1つのJAX-RPC呼び出しは、2本の直線で表現される2つのSOAPメッセージ(要求と応答)として示されています。WSDL文書は、要求と応答の両方に対するサービス・インターフェース(そしてSOAPのコンテント)を定義します。
JAX-RPC SOAPハンドラーをメッセージ・フローに配置する場合、リモート・プロシージャー・コール(RPC)に関与する両方のメッセージ(要求と応答)とのアクセスを得ることができます。handleRequest()メソッド、handleResponse()メソッド、そしてhandleFault()メソッドを使ってRPC呼び出しのモデルをサポートすると言う意味では、JAX-RPC SOAPハンドラーはJAX-RPCハンドラーなのです。メッセージのコンテントが(SAAJオブジェクトの形式のSOAPコンテントとして)ハンドラーに表現されると言う意味では、それ(JAX-RPC SOAPハンドラー)はSOAPハンドラーです。(このオブジェクトの詳細については、参考文献を参照して下さい。)
ハンドラーは原始的なSOAPコンテントを直接操作し、WSDLに定義される予測されたコンテントの存在に気付きません。強く型定義されたJAX-RPCサービス・インターフェースとは対照的に、ハンドラーの開発者はSOAPメッセージの妥当性に対して責任を抱くことになります。
クライアントとサービスの間で交わされる帯域外の情報を扱うチャネルを、SOAPヘッダーは提供します。インターフェース(そしてその結果、WSDL)を変更せずして、サービス・インターフェースに示される振る舞いを拡張する機会を、追加情報の交換は提供します。WSDLにより定義される必要がSOAPヘッダーにはないからこそ、これは有効なのです。(これについての詳細は、参考文献にリンクされているWS-I Basic Profile Version 1.0aを参照して下さい。)
WSDL/Schemaに定義されていないSOAPヘッダーでメッセージが拡張されるのですが、SOAPヘッダーのコンテントは輪郭をはっきりとさせるべきです。このヘッダーに関わる全ての送信側(producers)と受信側(consumers)はそのコンテントに呼応しなくてはなりません。
JAX-RPC SOAPハンドラーは、そのような帯域外情報を扱う操作手段です。図2に示されるとおり、ハンドラーをクライアントそして対応するハンドラーをサービスに配置することにより、そのような情報を明確に扱えます。
図2. ハンドラー使用時のJAX-RPC/SOAPメッセージ・フロー
SOAPヘッダー情報と関連して、2つのハンドラーに示される4つの処理ポイントは、以下のとおりです。
-
クライアント側
handleRequest():発信される要求メッセージに拡張されたコンテントを添付します。 -
サーバー側
handleRequest():着信された要求メッセージから拡張されたコンテントを抽出します。 -
サーバー側
handleResponse()/handleFault():発信される応答メッセージに拡張されたコンテントを添付します。 -
クライアント側
handleResponse()/handleFault():着信された応答メッセージから拡張されたコンテントを抽出します。
それぞれのハンドラー内にある要求(request)メッセージから応答(response)メッセージへの(MessageContextを使用した)情報交換のポイントに注目してください。これは「How to create a simple JAX-RPC handler」と言うタイトルの記事(リンクが参考文献にあります。)で考察されています。
特定の例に示される状況を踏まえて、この概念を探求しましょう。
この簡単な例では、シグニチャーをヘッダーに配置することにより発信されるメッセージにサイン(署名)をし、着信されるメッセージのシグニチャーが正しいことを検証する協力的なハンドラーを配置します。シグニチャーはメッセージの本文を象徴します。本文が変更されれば、シグニチャー検証は失敗します。署名者の名前はハンドラーの構成に入っているべきです。(クライアントとサービスが同じ名前を使ってサインする必要はありません。)
SignatureToolインターフェースで表記されるメッセージをサインするためのアルゴリズムが必要です。クライアント・ハンドラーとサービス・ハンドラーの双方は発信されるメッセージをサインし着信されるメッセージを検証する必要がありますので、抽象クラスSignHandlerが共通コードを提供します。ClientSignHandler そしてServiceSignHandlerはSignHandlerを拡張し、全てをうまくまとめます。
リスト1. SignatureTool.java
interface SignatureTool {
/**
* @return Result of signer signing content.
*/
public SOAPElement getSignature(String signersName,
SOAPElement content) throws SOAPException;
/**
* @return true if content was signed, unchanged, by signer of signature.
* This is only true if the content is the same content signed originally
* by a signer, resulting in signature.
*/
public boolean isSignatureValid(SOAPElement signature,
SOAPElement content) throws SOAPException;
}
|
発信されるSOAPメッセージから抽出されるコンテントと共にSignatureTool.getSignature()を呼び出すことにより、ハンドラーはメッセージをサインできます。同様に、着信されるSOAPメッセージから抽出されるコンテントそしてシグニチャーと共にisSignatureValid()を呼び出すことにより、シグニチャーの検証は実行されます。
シグニチャーを新規のヘッダー・ブロックとしてメッセージに追加することにより、発信されるメッセージを処理します。(リスト2にて表記される)SignHandler.signOutgoing()は、どのようにしてヘッダー・ブロックを発信されるSOAPメッセージに追加するかを実例で示します。このメソッドは次のことをします。
- エンベロープ内の
SOAPHeaderを探し出します。 - 子SOAPヘッダー要素を追加します。
- 新規ヘッダー要素のコンテントを作成そして設定します。
リスト2. SignHandler.signOutgoing()
abstract class SignHandler implements Handler
{
. . .
/**
* Name of required property, in HandlerInfo handler configuration,
* that specifies who signs outgoing messages.
*/
public static String SIGNERS_NAME_PROPERTY;
private SignatureTool signatureTool;
private HandlerInfo info;
/**
* Obtain signer's name from handler configuration (HandlerInfo),
* and sign message.
* @throws SignException if SIGNERS_NAME_PROPERTY is not available
* in the handler config.
*/
public void signOutgoing(SOAPMessageContext mc) throws SignException {
// SIGNERS_NAME_PROPERTY is required to be on the configuration.
Map config = info.getHandlerConfig();
Object nameObj = config.get(SIGNERS_NAME_PROPERTY);
String name = (String)nameObj;
try {
// Dig down into message, locate or create header block.
SOAPMessage msg = mc.getMessage();
SOAPPart part = msg.getSOAPPart();
SOAPEnvelope envelope = part.getEnvelope();
SOAPHeader header = envelope.getHeader();
/**
* Create new header element.
* We don't specify a role on this header element,
* meaning the target role is the "ultimate destination".
*/
SOAPHeaderElement headerElement
= (SOAPHeaderElement)header.addChildElement(SIGN_ELEMENT,
SIGN_PREFIX,
SIGN_NS_URI);
// Locate portion of message content that is to be signed.
SOAPElement content = getContent(part);
/**
* Create new element representing signature,
* and add as child to new header element.
*/
SOAPElement element = signatureTool.getSignature(name, content);
headerElement.addChildElement(element);
} catch (SOAPException e) {
e.printStackTrace();
throw new SignException("Unable to sign message", e);
}
}
. . .
// See resources for full sample code.
}
|
処理後には、SOAPエンベロープはリスト3に表記されているようになります。
リスト3. アウトバウンドSOAPメッセージ
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<!-- original header content -->
<!-- New header element, inserted by handler -->
<sign:sign xmlns:sign="uri://org.example.webservices.signature.Sign">
<!-- output of SignatureTool.getSignature() -->
</sign:sign>
</soap:Header>
<soap:Body>
<!-- original body content -->
</soap:Body>
</soap:Envelope>
|
シグニチャーが有効であることを検証するために、着信メッセージを処理します。(リスト4で表記される)SignHandler.checkIncoming()は、どのようにしてヘッダー・ブロックを探し出すか(明白ではない処理)を実例で示します。このメソッドは次のことをします。
- エンベロープ内の
SOAPHeaderを探し出します。 - 現行のSOAPノードが演じる役割を確定します。(詳細は下記にて)
- 現行のハンドラーが理解するヘッダーのセットを確定します。
- このハンドラーが理解し、現行のSOAPノードが演じる役割に義務付けられるヘッダーを処理します。
リスト4. SignHandler.checkIncoming()
abstract class SignHandler implements Handler
{
. . .
/**
* Name of required property, in HandlerInfo handler configuration,
* that specifies who signs outgoing messages.
*/
public static String SIGNERS_NAME_PROPERTY;
private static QName SIGN_HEADER;
private SignatureTool signatureTool;
private HandlerInfo info;
/**
* Look for signature on incoming message.
* If signature not found, then continue message processing.
* If signature is found, then verify that it is "correct".
* If correct, then continue message processing.
* If not correct, then throw SignException.
*/
public void checkIncoming(SOAPMessageContext mc) throws SignException {
try {
SOAPMessage msg = mc.getMessage();
SOAPPart part = msg.getSOAPPart();
SOAPEnvelope envelope = part.getEnvelope();
/**
* Locate portion of message content that is to be signed.
*/
SOAPElement content = getContent(part);
/**
* Dig down through SOAP headers looking for matches to
* SIGN_HEADER, that are also targeted for this SOAP Node.
*/
SOAPHeader header = envelope.getHeader();
if (header != null) {
/**
* The roles the node acts in are specified by the
* MessageContext.getRoles() method.
* If roles not found, default to "ultimate destination".
*/
String[] roles = mc.getRoles();
if (roles == null || roles.length == 0) {
roles = new String[] { "" };
}
for (int ridx = 0; ridx < roles.length; ridx++) {
String role = roles[ridx];
/**
* Examine headers bound to each role this node acts in.
* Headers are determined to be targeted for a SOAP Node by
* matching the node's roles with the header's actor role.
*
* So now go through list of headers associated with
* the role we are currently working on.
*/
Iterator headerElementIter
= header.examineHeaderElements(role);
while (headerElementIter.hasNext()) {
SOAPHeaderElement headerElement
= (SOAPHeaderElement)headerElementIter.next();
// Is header recognized by this handler?
Name headerElementName
= headerElement.getElementName();
if (equals(headerElementName, SIGN_HEADER)) {
/**
* Look for SOAPElement(s) in header,
* ignoring mixed content.
*/
Iterator headerIter
= headerElement.getChildElements();
while (headerIter.hasNext()) {
Object elementObj = headerIter.next();
if (elementObj instanceof SOAPElement) {
SOAPElement element
= (SOAPElement)elementObj;
signatureTool.isSignatureValid(element,
content);
}
}
}
}
}
}
} catch (SOAPException e) {
e.printStackTrace();
throw new SignException("Unable to verify signature", e);
}
}
. . .
// See resources for full sample code.
}
|
役割についての追加事項
SOAPノードが演じる役割はMessageContextから入手できると予測されます。リスト4に示されるコードは、WebSphere Application Serverが空のストリングを最終宛先(デフォルトの実行者)にマッピングすることを実例で示します。最終宛先の適切な値はSOAP 1.1仕様により明確に指定されていませんので、これは実装により異なります。これについては、今後発行される記事にてより詳しい考察をする予定です。
SignHandlerクラスは、着信そして発信されるメッセージを処理するように指向されています。ハンドラーのインターフェースは要求そして応答メッセージを処理するように指向されています。この2つはハンドラー実装にてひとつにまとめられます(一緒に機能します)。
リスト5にて表記されるClientSignHandlerは到って簡単です。処理用の要求メッセージ・コンテキストを発信メッセージ、そして処理用の応答メッセージと障害メッセージを着信メッセージとして誘導します。これは、ServiceSignHandlerを補います。
リスト5. ClientSignHandler
public class ClientSignHandler extends SignHandler
{
public boolean handleRequest(MessageContext mc) {
signOutgoing((SOAPMessageContext)mc);
return true;
}
public boolean handleResponse(MessageContext mc) {
checkIncoming((SOAPMessageContext)mc);
return true;
}
public boolean handleFault(MessageContext mc) {
checkIncoming((SOAPMessageContext)mc);
return true;
}
}
|
リスト6にて表記されるServiceSignHandlerも同様に簡単です。処理用の要求メッセージ・コンテキストを着信メッセージ、そして処理用の応答メッセージと障害メッセージを発信メッセージとして誘導します。これは、ClientSignHandlerを補います。
リスト6. ServiceSignHandler
public class ServiceSignHandler extends SignHandler
{
public boolean handleRequest(MessageContext mc) {
checkIncoming((SOAPMessageContext)mc);
return true;
}
public boolean handleResponse(MessageContext mc) {
signOutgoing((SOAPMessageContext)mc);
return true;
}
public boolean handleFault(MessageContext mc) {
signOutgoing((SOAPMessageContext)mc);
return true;
}
}
|
この例を完全なものにするには、ハンドラーを構成し配置します。署名者を指定する構成プロパティーをハンドラーは探します。さらに重要なことに、ハンドラーに処理された全てのヘッダー要素がデプロイメント情報に指定されることをランタイムは予測します。
クライアントには、JAX-RPCプログラム・インターフェースを使い、ハンドラーを配置します。サービスには、Webサービスのデプロイメント記述子用のJ2EEを使いハンドラーを配置します。
JAX-RPC 1.0プログラム構成:HandlerInfo()
リスト7にて表記されるHandlerInfo()は、(ハンドラーが処理すると予測される)ヘッダー要素、ハンドラー、そしてその構成を定義します。この例では、ヘッダー要素は静的なプロパティーClientSignHandler.HEADERSとして公開されています。
リスト7. HandlerInfo()のセットアップ
Map hConfig = new HashMap();
hConfig.put(ClientSignHandler.SIGNERS_NAME_PROPERTY, "Linus");
HandlerInfo hInfo = new HandlerInfo(ClientSignHandler.class,
hConfig,
ClientSignHandler.HEADERS);
|
ハンドラーを登録するには、ハンドラー・チェーンの最後にHandlerInfo()を追加(添付)します。ハンドラー・チェーンに付随する(しない)ハンドラーについての想定を無理にしないことが、良い慣例と言えます。
リスト8. ハンドラーの登録
// Obtain service, for JAX-RPC 1.0 this may be implementation specific.
StockQuoteService service = ...;
// Must match WSDL's port QName
QName portQName = new QName("http://stock.webservices.example.org",
"StockQuote");
service.getHandlerRegistry().getHandlerChain(portQName).add(hInfo);
|
J2EE 1.3デプロイメント・モデルのWebサービス:webservices.xml
J2EEに管理された環境では、プログラム・インターフェースの使用は例外を生じさせます。ポートに配置されたハンドラーを指定するために、Webサービス(webservices.xml)のJ2EE デプロイメント記述子を使用しなくてはなりません。
Webサービス・デプロイメント記述子は、HandlerInfo()を使用しプログラム的に設定された情報を反映します。(名前(name)と値(value)の組み合わせの)構成プロパティー(init-param)、ハンドラー・クラス名(handler-class)、そしてハンドラーが理解するヘッダー(soap-header)を指定します。これらがどのように組み込まれているかを、リスト9にて観察できます。
リスト9. webservices.xml
. . .
<port-component>
<port-component-name>StockQuote</port-component-name>
<wsdl-port>
<namespaceURI>http://stock.webservices.example.org</namespaceURI>
<localpart>StockQuote</localpart>
</wsdl-port>
. . .
<handler>
<handler-name>Signature Handler</handler-name>
<handler-class>org.example.webservices.signature.
ServiceSignHandler</handler-class>
<init-param>
<param-name>org.example.webservices.signature.SignersName</param-name>
<param-value>Snoopy</param-value>
</init-param>
<soap-header>
<namespaceURI>uri://
org.example.webservices.signature.Sign</namespaceURI>
<localpart>sign</localpart>
</soap-header>
</handler>
</port-component>
. . .
|
ハンドラーの設計そして実装に関連するベスト・プラクティスをいくつか紹介しました。
- 状況に応じてメッセージ・フローの対称性をてこ入れする。サービスの応答・障害処理に反映されるクライアント要求処理、そしてクライアントの応答・障害処理に反映されるサービス要求処理は、ハンドラー開発にて繰り返されるパターンです。共通のコードをてこ入れするために、アウトバウンドとインバウンドのパターンを基にした共通の実装を提供します。
-
ヘッダー・コンテントの処理から
SOAPHeaderElementの処理を分離する:このサンプルでは、ヘッダー・コンテントの処理からSOAPHeaderElementの処理を分離しました。ハンドラーがSOAPHeaderElementを探し出し処理します。ヘッダー要素のコンテントの構造そして処理は、1つのSOAPElementにコンテントを抽象化することにより、SignatureToolに委ねられました。 -
理解されたヘッダーは、
HandlerInfoまたはデプロイメント記述子により定義されるべきです。 これはベスト・プラクティスと言うよりもルール(常識)です。ハンドラーに処理される予定のヘッダー全ては、HandlerInfoまたは適切なデプロイメント記述子を介してランタイムへ定義されるべきです。JAX-RPCは、ハンドラーに作成されたヘッダーには類似する構成のポイントを提供しません。このトピックは今後発行される秘訣記事にて取り上げます。
アウトバウンドそしてインバウンドのメッセージを処理するために協力するクライアント・ハンドラーとサービス・ハンドラーは、現存するSOAPをベースにしたWebサービスを拡張します。アウトバウンド・メッセージの処理は比較的単刀直入ですが、インバウンド・メッセージの処理は『自明の理』だとは言えません。この記事では、インバウンド・メッセージの処理を示すロードマップを紹介し、それぞれのハンドラーに処理される予定のヘッダー要素を基にランタイムが構成されることを説明しました。
- この記事で使用されたソース・コードをダウンロードしましょう。zipファイルはクライアントJARファイルとサービスWARファイルを含み、双方ともにソース・コードが入っています。
-
Simple Object Access Protocol (SOAP) 1.1仕様は、SOAPメッセージのコンテントそしてSOAPノードの処理モデルの概略を説明します。
-
WS-I Basic Profile Version 1.0a(特に、SOAPヘッダーとWSDLの定義の関連について)をお読みください。
- Andre Tost著の「Using SOAP headers with JAX-RPC」(developerWorks、2003年10月)をお読みください。
- Andre Tost と Russell Butekによる「簡単なJAX-RPC ハンドラーの作成方法」(developerWorks、2003年11月)をお読みください。
-
The SOAP with Attachments API for Java (SAAJ)で、開発者は添付文書付きのSOAP 1.1メッセージを制作そして消費できるようになります。
Richard A. Sitzeは、IBM WebSphere Webサービス開発チームの一員です。これまでにApache Axis SOAPエンジン、Jakarta Commons Logging、そしてJakarta Commons Discoveryのオープン・ソース・プロジェクトに関与してきました。CORBA ORBインターオペラビリティーとインターネット・バンキング・サービスで、IBMでの実績をあげました。IBMの前には、ビジネス・システム、リアルタイム制御システム、ファームウェア、ネットワーク通信プロトコル、スレッド化カーネル、そしてマルチプロセッサーUNIXカーネルの開発に携わりました。