Webサービス・プログラミングのヒントと秘訣: SOAPヘッダーを活用してJAX-RPC Webサービスを拡張

JAX-RPC 1.0 SOAPハンドラーを使い、SOAP 1.1ヘッダーを処理

JAX-RPC SOAPハンドラーがどのようにしてSOAPメッセージ・ヘッダーを処理するかを、著者はこの記事で考察します。特に、ハンドラーが発信されるメッセージにどのようにしてSOAPヘッダーを追加するか、そしてそれに対応するハンドラーがどのようにしてSOAPヘッダーを着信メッセージから抽出するかを説明します。さらに、この記事に関連するJAX-RPCのプログラム構成そしてデプロイメント・モデルを紹介します。

Richard Sitze, Advisory Software Engineer, IBM Software Group

Richard A. Sitzeは、IBM WebSphere Webサービス開発チームの一員です。これまでにApache Axis SOAPエンジン、Jakarta Commons Logging、そしてJakarta Commons Discoveryのオープン・ソース・プロジェクトに関与してきました。CORBA ORBインターオペラビリティーとインターネット・バンキング・サービスで、IBMでの実績をあげました。IBMの前には、ビジネス・システム、リアルタイム制御システム、ファームウェア、ネットワーク通信プロトコル、スレッド化カーネル、そしてマルチプロセッサーUNIXカーネルの開発に携わりました。



2004年 4月 28日

SOAP ヘッダーを追加することにより、WSDLに定義されたWebサービスをどのように拡張できるかを、この記事で説明をします。SOAPハンドラーがどのようにしてSOAPメッセージ・ヘッダーを作成して処理するか、そしてハンドラーをどのように適切に構成するかを示します。

基本的なJAX-RPC SOAPハンドラー

まずは、図1に示されるJAX-RPCの呼び出しを考察することから始めます。

図1. JAX-RPC/SOAPメッセージ・フロー(メッセージの流れ)
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ハンドラーです。(このオブジェクトの詳細については、参考文献を参照して下さい。)

J2EE WebサービスのSOAPハンドラー

(JAX-RPCを基にした)J2EE Webサービス・ランタイムは、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メッセージ・フロー
ハンドラー使用時の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 そしてServiceSignHandlerSignHandlerを拡張し、全てをうまくまとめます。

リスト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 SOAPハンドラーを構成する

この例を完全なものにするには、ハンドラーを構成し配置します。署名者を指定する構成プロパティーをハンドラーは探します。さらに重要なことに、ハンドラーに処理された全てのヘッダー要素がデプロイメント情報に指定されることをランタイムは予測します。

クライアントには、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サービスを拡張します。アウトバウンド・メッセージの処理は比較的単刀直入ですが、インバウンド・メッセージの処理は『自明の理』だとは言えません。この記事では、インバウンド・メッセージの処理を示すロードマップを紹介し、それぞれのハンドラーに処理される予定のヘッダー要素を基にランタイムが構成されることを説明しました。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=SOA and web services, XML
ArticleID=245082
ArticleTitle=Webサービス・プログラミングのヒントと秘訣: SOAPヘッダーを活用してJAX-RPC Webサービスを拡張
publish-date=04282004