SeamFramework.org Community Documentation

- 前のページ
- 次のページ

## 第24章 Webサービス

SeamとJBossWSを統合することで、標準のJEE のWebサービスにたいして、対話型Webサービスにも対応したSeamのコンテキストフレームワークを十分に活用することができます。本章では、Seam環境でWebサービスが動作するのに必要な手順を説明します。

## 24.1. 設定とパッケージング

Webサービス要求をSeamがインタセプトできるように、必要なSeamのコンテキストがその要求に合わせて生成できなければなりませんが、それにはSOAPハンドラを特別に設定する必要があります。`org.jboss.seam.webservice.SOAPRequestHandler`は`SOAPHandler`実装として、Webサービス要求のスコープ中にSeamのライフサイクルを管理するのに使われています。

特殊な設定ファイルである`standard-jaxws-endpoint-config.xml`は、Webサービスクラスを含むjarファイルの`META-INF`ディレクトリに配置する必要があります。このファイルには、以下のようなSOAPハンドラの設定が含まれています。

    <jaxws-config xmlns="urn:jboss:jaxws-config:2.0"

                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

                  xmlns:javaee="http://java.sun.com/xml/ns/javaee"

                  xsi:schemaLocation="urn:jboss:jaxws-config:2.0 jaxws-config_2_0.xsd">

       <endpoint-config>

          <config-name

    >Seam WebService Endpoint</config-name>

          <pre-handler-chains>

             <javaee:handler-chain>

                <javaee:protocol-bindings

    >##SOAP11_HTTP</javaee:protocol-bindings>

                <javaee:handler>

                   <javaee:handler-name

    >SOAP Request Handler</javaee:handler-name>

                   <javaee:handler-class

    >org.jboss.seam.webservice.SOAPRequestHandler</javaee:handler-class>

                </javaee:handler>

             </javaee:handler-chain>

          </pre-handler-chains>

       </endpoint-config>

    </jaxws-config

    >

## 24.2. 対話型Webサービス

では、Webサービス要求間でどのように対話が伝播されているのでしょう？ Seamでは、SOAP要求と応答メッセージの両方でSOAPヘッダー要素を使い、そのconversation IDをコンシューマからサービスへ、またサービスからコンシューマへと伝えています。以下はconversation IDを含むWebサービスの要求の一例です。

    <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"

        xmlns:seam="http://seambay.example.seam.jboss.org/">

      <soapenv:Header>

        <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'

    >2</seam:conversationId>

      </soapenv:Header>

      <soapenv:Body>

        <seam:confirmAuction/>

      </soapenv:Body>

    </soapenv:Envelope

    >

上記のSOAPメッセージで見られるように、SOAPヘッダー内にその要求のためのconversation ID（ここでは`2`）を持つ`conversationId`要素があります。残念ながら、Webサービスを使うクライアントは多種多様で、なおかつさまざまな言語で記述されているため、ひとつの対話のスコープ内で使われると想定されるconversation IDの伝播をどのように実装するかは、個々のWebサービス間の開発者次第です。

ここで重要なのは、 `conversationId`ヘッダー要素はhttp://www.jboss.org/seam/webserviceの名前空間に適したものでなければいけません。そうでなければ、Seamはその要求からconversation IDを読み取ることができなくなってしまいます。上記の要求メッセージに対する応答の一例を以下に示します。

    <env:Envelope xmlns:env='http://schemas.xmlsoap.org/soap/envelope/'>

      <env:Header>

        <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'

    >2</seam:conversationId>

      </env:Header>

      <env:Body>

        <confirmAuctionResponse xmlns="http://seambay.example.seam.jboss.org/"/>

      </env:Body>

    </env:Envelope

    >

ここにあるように、応答メッセージには要求と同じ`conversationId`要素が含まれています。

### 24.2.1. 推奨される方法

Webサービスは、ステートレスセッションBeanもしくはPOJOとして実装する必要があります。このため、対話型Webサービスでは、対話型SeamコンポーネントのファサードとしてそのWebサービスが動作することが望ましいでしょう。

ステートレスセッションBeanで記述されたWebサービスの場合、`@Name`を指定してSeamコンポーネントにすることも可能です。こうすることで、Seamのバイジェクション等の機能がWebサービスのクラスで使えるようになります。

## 24.3. Webサービスの例

Webサービスの一例を見てみましょう。ここで例示するコードは、すべてSeamの`/examples`ディレクトリにあるseamBayの例から引用したもので、前節で述べた推奨される方法に添っています。まず、Webサービスのクラスとそのメソッドから見てみましょう。

    @Stateless

    @WebService(name = "AuctionService", serviceName = "AuctionService")

    public class AuctionService implements AuctionServiceRemote

    {

       @WebMethod

       public boolean login(String username, String password)

       {

          Identity.instance().setUsername(username);

          Identity.instance().setPassword(password);

          Identity.instance().login();

          return Identity.instance().isLoggedIn();

       }

       // snip

    }

ここで、WebサービスはステートレスセッションBeanで、JSR-181で定義されている通り、`javax.jws`パッケージのJWSアノテーションを使ってアノテートされています。`@WebService`アノテーションは、このクラスがWebサービスを実装していることをコンテナに伝えます。そして、 `login()`メソッドの`@WebMethod`アノテーションがWebサービスとしてのメソッドを定義しています。`@WebService`アノテーションのnameと`serviceName`属性はオプションです。

仕様書にある通り、Webサービスのメソッドとして指定された各メソッドは、そのWebサービスのクラスのリモートインタフェース中でも宣言しておく必要があります。（WebサービスがステートレスセッションBeanの場合） 上記の例では、`AuctionServiceRemote`インタフェースが`@WebMethod`としてアノテートされているため、`login()`メソッドを宣言しなければなりません。

上記のコードにあるように、Webサービスが実装する`login()`メソッドは、Seamの組み込みIdentityコンポーネントに委譲されています。前節で推奨した方法を踏まえると、単にファサードとして記述されたWebサービスは、実際の作業をSeamコンポーネントに流します。これによって、Webサービスとクライアント間でビジネスロジックを最大限に再利用できます。

もう一つの例を見てみましょう。このWebサービスメソッドは、`AuctionAction.createAuction()`メソッドに委譲されることで新しい対話が始まります。

       @WebMethod

       public void createAuction(String title, String description, int categoryId)

       {

          AuctionAction action = (AuctionAction) Component.getInstance(AuctionAction.class, true);

          action.createAuction();

          action.setDetails(title, description, categoryId);

       }

以下は、`AuctionAction`からのコードです。

       @Begin

       public void createAuction()

       {

          auction = new Auction();

          auction.setAccount(authenticatedAccount);

          auction.setStatus(Auction.STATUS_UNLISTED);

          durationDays = DEFAULT_AUCTION_DURATION;

       }

これにより、Webサービスがファサードとして実際の作業を対話型Seamコンポーネントに委譲することで、長い対話を続けていることが判ります。

## 24.4. RESTEasy によるRESTful HTTP Webサービス

SeamはJAX-RS 仕様(JSR 311)にあるRESTEasyの実装を組み込んでいます。これをSeamアプリケーションのどこまで”深く”取り入れるかは、以下のように自分で設定することができます。

- RESTEasy ブートストラップと設定をシームレスに組み込み、リソースとプロバイダを自動検出。

- SeamResourceServlet によりHTTP/REST 要求を処理し、外部サーブレットやweb.xml での設定は不要。

- Seam コンポーネントとしてリソースを記述することで、Seamのライフサイクル管理とインタセプション（バイジェクション）をすべて利用可能。

### 24.4.1. RESTEasy の設定と要求

まず始めに、RESTEasyライブラリと `jaxrs-api.jar`を取得し、あなたのアプリケーションにある他のライブラリと一緒にデプロイします。さらに、インテグレーションライブラリ`jboss-seam-resteasy.jar`をデプロイします。

起動時には、`@javax.ws.rs.Path`でアノテートされた全クラスが自動的に検出され、HTTPリソースとして登録されます。Seamは組み込まれた`SeamResourceServlet`を使って自動的にHTTP要求を処理します。リソースのURIは以下のようにビルドされます。

- 一般的な例に合わせて説明すると、URIは`SeamResourceServlet`に対して`web.xml`内でマップしてあるパターン（例えば、`/seam/resource`）で始まります。ベースが異なる場合、RESTful リソースが処理されるように、この設定を変更してください。この変更はグローバルなものなので、他のSeamリソース（例えば、`s:graphicImage`）もそのベースパスで処理されることに注意してください。

- 次にSeamのRESTEasy の組み込みで、このベースパスに任意の文字列を追加します。デフォルトでは `/rest`になっています。従って、この例の場合、リソースのフルベースパスは`/seam/resource/rest`になります。アプリケーションで使う場合、例えば将来的にそのサービスのREST API をアップグレードすることを考慮して、バージョン番号を追加する等して（`/seam/resource/restv1`）、この文字列は変えておいたほうが良いでしょう。（古いクライアントは元のURIベースを保持します）

- これで、 実際のリソースは定義した`@Path`以下で利用できるようになります。例えば、`@Path("/customer")`でマップしたリソースは、`/seam/resource/rest/customer`以下で利用できます。

一例として、以下のリソース定義では`http://your.hostname/seam/resource/rest/customer/123`というURIを使った、いかなるGET要求に対してもプレーンテキスト表示を返します。

    @Path("/customer")

    public class MyCustomerResource {

        @GET

        @Path("/{customerId}")

        @Produces("text/plain")

        public String getCustomer(@PathParam("customerId") int id) {

             return ...;

        }

    }

他に設定する必要はありません。つまり、以上のようにデフォルトで問題なければ、`web.xml`や他の設定を変更する必要はありません。あるいは、自分のSeamアプリケーションのRESTEasyの設定を変えても良いでしょう。まず、`resteasy`名前空間をXML設定ファイルのヘッダーにインポートします。

    <components

       xmlns="http://jboss.com/products/seam/components"

       xmlns:resteasy="http://jboss.com/products/seam/resteasy"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation=

         http://jboss.com/products/seam/resteasy

             http://jboss.com/products/seam/resteasy-2.2.xsd

         http://jboss.com/products/seam/components

             http://jboss.com/products/seam/components-2.2.xsd"

    >

次に、先に述べたように`/rest`プレフィックスを変更できます。

    <resteasy:application resource-path-prefix="/restv1"/>

この場合、リソースへのフルベースパスは`/seam/resource/restv1/{resource}`になります。ここで`@Path`の定義とマッピングは変更しません。これは通常HTTP APIのバージョニングに使われる、アプリケーション全体のスイッチです。

リソース中においてフルパスでマップしたい場合は、ベースパスのストリッピングを無効にすることができます。

    <resteasy:application strip-seam-resource-path="false"/>

これにより、リソースのパスは、例えば`@Path("/seam/resource/rest/customer")`でマップされています。この場合、リソースクラスのマッピングは、特定のデプロイメントシナリオに制限されるので、この機能は無効にしないほうが良いでしょう。

デプロイされたすべての`@javax.ws.rs.Path`リソースおよび`@javax.ws.rs.ext.Provider`クラスに対し、Seamはクラスパスをスキャンします。このスキャンを無効化し、マニュアルでクラスを設定することもできます。

    <resteasy:application

         scan-providers="false"

         scan-resources="false"

         use-builtin-providers="true">

         <resteasy:resource-class-names>

             <value

    >org.foo.MyCustomerResource</value>

             <value

    >org.foo.MyOrderResource</value>

             <value

    >org.foo.MyStatelessEJBImplementation</value>

         </resteasy:resource-class-names>

         <resteasy:provider-class-names>

             <value

    >org.foo.MyFancyProvider</value>

         </resteasy:provider-class-names>

     </resteasy:application

    >

`use-built-in-providers`は、RESTEasy組み込みプロバイダを有効もしくは無効にします。（デフォルトでは有効） プレーンテキストやJSON、JAXBをそのままで整列化できるので、この機能は有効にしておくと良いでしょう。

RESTEasy は、リソースとして、(Seam コンポーネントではない）ありきたりな EJBs をサポートします。 `web.xml` で移植性の低い JNDI 名を定義する (RESTEasy ドキュメントを確認してください) 代わりに、上に示したように `components.xml` で単に ビジネスインタフェースではなく、EJB の実装クラスをリストアップするだけです。その EJB の実装クラスではなく、 `@Local` インタフェースを `@Path`, `@GET` などでアノテーションを付加する必要があることに注意してください。これにより、`<core:init/>` で、Seam 全体に適用される `jndi-pattern` を指定することによってあなたのアプリケーションのデプロイの移植性を保つことが可能になります。EJB リソースはリソーススキャンが有効であったとしても見つけられないことに注意してください。常に手動でリストしなければなりません。繰り返しとなりますが、これは Seam コンポーネントではなく、`@Name` アノテーションを持たない、EJB リソースにのみ関連するものです。

最後に、メディアタイプと言語のURI拡張子を設定できます。

    <resteasy:application>

        <resteasy:media-type-mappings>

           <key

    >txt</key

    ><value

    >text/plain</value>

        </resteasy:media-type-mappings>

        <resteasy:language-mappings>

           <key

    >deutsch</key

    ><value

    >de-DE</value>

        </resteasy:language-mappings>

    </resteasy:application

    >

この定義によって、`.txt.deutsch` というURIサフィックスに、付加的な`Accept`および`Accept-Language`のヘッダー値である`text/plain`と`de-DE`をマップすることができます。

### 24.4.2. Seam コンポーネントとしてのリソースとプロバイダ

どのリソースやプロバイダのインスタンスも、デフォルトでRESTEasyで管理されています。つまり、あるリソースのクラスをRESTEasyがインスタンス化し、ある要求が処理された後に、そのクラスは破棄されます。これがデフォルトのJAX-RSのライフサイクルになっています。プロバイダに関しては、アプリケーション全体に対して一度インスタンス化され、効率的に、シングルトンとなり、またステートレスになるよう想定されています。

リソースやプロバイダをSeamコンポーネントとして記述することもできます。こうすることでSeamのより豊富なライフサイクル管理のほか、バイジェクションやセキュリティ等のインタセプションを利用することができます。リソースのクラスをSeamコンポーネントにするのは簡単で、以下のようにします。

    @Name("customerResource")

    @Path("/customer")

    public class MyCustomerResource {

        @In

        CustomerDAO customerDAO;

        @GET

        @Path("/{customerId}")

        @Produces("text/plain")

        public String getCustomer(@PathParam("customerId") int id) {

             return customerDAO.find(id).getName();

        }

    }

これで、要求がサーバーに届くと `customerResource` のインスタンスを Seam が処理するようになります。これは `EVENT` スコープな Seam JavaBean コンポーネントなので、デフォルトの JAX-RS ライフサイクルと違いはありません。これですべての Seam インジェクションやインタセプトのサポートやその他の Seam コンポーネントおよびコンテキストが利用できるようになります。現在、加えて `APPLICATION` 、および `STATELESS` のリソースコンポーネントがサポートされています。これら三つのスコープにより、Seam を使って効果的にステートレスの HTTP 要求処理アプリケーションを中間層に作成することができます。

インタフェースにアノテーションを付加することで、その実装には JAX-RS アノテーションを付ける必要がなくなります。

    @Path("/customer")

    public interface MyCustomerResource {

        @GET

        @Path("/{customerId}")

        @Produces("text/plain")

        public String getCustomer(@PathParam("customerId") int id);

    }

    @Name("customerResource")

    @Scope(ScopeType.STATELESS)

    public class MyCustomerResourceBean implements MyCustomerResource {

        @In

        CustomerDAO customerDAO;

        public String getCustomer(int id) {

             return customerDAO.find(id).getName();

        }

    }

あなたは、`SESSION` スコープのSeamコンポーネントを使うことができます。しかしながら、標準では、そのセッションは単一の要求に短縮されます。別の言い方をすると、HTTP 要求が RESTEasy 統合コードで処理されている場合、HTTP セッションが作成されて、Seam コンポーネントがそのコンテキストを利用することができます。その要求が処理されると、Seam はそのセッションを見て、セッションが単一要求を提供するためだけに作成されたのかどうか（セッション識別子がその要求で提供されていなかったりセッションがその要求に対して存在していなかったりする）とを判断します。そのセッションがこの要求を提供するためだけに作成されていれば、そのセッションはその要求が完了すると破棄されます。

あなたの Seam アプリケーションがイベント、アプリケーション、ステートレスコンポーネントだけを利用している場合を仮定すると、この手続きはサーバーで 利用できる HTTP セッションの枯渇を防止します。デフォルトでは、Seam との RESTEasy インテグレーションはセッションが使われないことを想定しています。それゆえに、REST 要求の度に貧弱なセッションが追加されてしまうことになります。REST 要求が開始するセッションはタイムアウト時にしか削除されないからです。

RESTful Seam アプリケーションが REST HTTP 要求をまたいでセッションを保持しなければならないなら、設定ファイルでこの振る舞いを無効にしてください。

    <resteasy:application destroy-session-after-request="false"/>

REST HTTP 要求毎に新しいセッションが作成され、そのセッションはタイムアウトか、コードの中で `Session.instance().invalidate()` を通して明示的にインバリデーションされた場合にだけ削除されるでしょう。要求を跨いだセッションコンテキストを有効にしたいのであれば、HTTP 要求と共に有効なセッション識別子を通せるかどうかどうかはあなたの責任となります。

`CONVERSATION` スコープのリソースコンポーネントとテンポラリなHTTPリソースへの対話のマッピングは計画されていますが、現在サポートされていません。

EJB セッションコンポーネントはサポートされます。常に実装クラスではなく、ローカルビジネスインタフェースにJAX-RS アノテーションを付加してください。その EJB は `STATELESS` でなければなりません。

プロバイダクラスは Seam コンポーネントであることもできますが、 `APPLICATION` スコープのプロバイダコンポーネントだけがサポートされます。インタフェースか実装を JAX-RS アノテーションを付加してください。EJB Seam コンポーネントは現在のところ、プロバイダとしてはサポート されません。POJOだけです！

### 24.4.3. セキュリティリソース

`components.xml` で Seam 認証フィルタを通して HTTP ベーシック認証とダイジェスト認証を有効にすることができます。

    <web:authentication-filter url-pattern="/seam/resource/rest/*" auth-type="basic"/>

認証ルーチンの記述の仕方については Seam セキュリティの章を見て下さい。

認証が成功したら、`@Restrict` アノテーションと `@PermissionCheck` アノテーションを使った認可のルールが有効になります。クライアントの `Identity` や権限のマッピングなどにもアクセスできるようになります。認可に関する Seam のセキュリティの機能のすべてが利用可能になるわけです。

### 24.4.4. HTTP 応答に対する例外のマッピング

JAX-RS 仕様の3.3.4項はチェック例外または未チェック例外が JAX RS の実装でどのように扱われるかを定義しています。JAX-RS で定義されたプロバイダの例外マッピングとは別に、Seam と RESTEasy の統合によって、Seam の `pages.xml` の中に HTTP 応答コードに対する例外の定義が可能です。既に `pages.xml` 定義を利用しているのであれば、多数の JAX RS の例外クラスを作るよりも良いメンテナンス性を期待できます。

Seam の例外ハンドリングは Seam フィルタが HTTP 要求に対して実行されることを必要とします。 `web.xml` の すべての 要求をフィルタすることを確認して下さい。Seam サンプルによっては、ある要求の URI パターンが REST 要求パスをカバーしていないかもしれませんが。次のサンプルは すべての HTTP 要求をインタセプトし、Seam の例外ハンドリングを有効にしています。

    <filter>

        <filter-name

    >Seam Filter</filter-name>

        <filter-class

    >org.jboss.seam.servlet.SeamFilter</filter-class>

    </filter>

    <filter-mapping>

        <filter-name

    >Seam Filter</filter-name>

        <url-pattern

    >/*</url-pattern>

    </filter-mapping

    >

リソースメソッドにより投げられた未チェックの `UnsupportedOperationException` を `501 Not Implemented` HTTP ステータスの応答に変換するためには、 `pages.xml` ディスクリプタに以下を付け加えます。

    <exception class="java.lang.UnsupportedOperationException">

        <http-error error-code="501">

            <message

    >The requested operation is not supported</message>

        </http-error>

    </exception

    >

カスタムの例外や、チェック例外も同様に制御されます。

    <exception class="my.CustomException" log="false">

        <http-error error-code="503">

            <message

    >Service not available: #{org.jboss.seam.handledException.message}</message>

        </http-error>

    </exception

    >

例外がおきた時にクライアントに対して HTTP エラーを送らなくても構いません。Seam は例外を Seam アプリケーションのビューにリダイレクトさせることができます。この機能は典型的にはREST API リモートクライアントではなく、人間のクライアント（Webブラウザ）のために利用されるので、 `pages.xml` 内の例外マッピング内で衝突がないように注意を払うべきです。

もし、 `web.xml` 定義の `<error-page>` のマッピングに追加のマッピングが書かれている場合、そのHTTP 応答はまだサーブレットコンテナを通っている事に注意してください。その時、HTTP ステータスコードは `200 OK` ステータスを持った HTML エラーページにマッピングされるでしょう。

### 24.4.5. リソースとプロバイダのテスト

Seam は RESTful アーキテクチャのためのユニットテストを作るのに、役立つユニットテストのユーティリティを含んでいます。 `SeamTest` を継承して、HTTP 要求／応答をエミュレートするために `ResourceRequestEnvironment.ResourceRequest` を使用してください。

    import org.jboss.seam.mock.ResourceRequestEnvironment;

    import org.jboss.seam.mock.EnhancedMockHttpServletRequest;

    import org.jboss.seam.mock.EnhancedMockHttpServletResponse;

    import static org.jboss.seam.mock.ResourceRequestEnvironment.ResourceRequest;

    import static org.jboss.seam.mock.ResourceRequestEnvironment.Method;

    public class MyTest extends SeamTest {

       ResourceRequestEnvironment sharedEnvironment;

       @BeforeClass

       public void prepareSharedEnvironment() throws Exception {

           sharedEnvironment = new ResourceRequestEnvironment(this) {

                @Override

                public Map<String, Object

    > getDefaultHeaders() {

                   return new HashMap<String, Object

    >() {{

                       put("Accept", "text/plain");

                   }};

                }

             };

       }

       @Test

       public void test() throws Exception

       {

          //Not shared: new ResourceRequest(new ResourceRequestEnvironment(this), Method.GET, "/my/relative/uri)

          new ResourceRequest(sharedEnvironment, Method.GET, "/my/relative/uri)

          {

             @Override

             protected void prepareRequest(EnhancedMockHttpServletRequest request)

             {

                request.addQueryParameter("foo", "123");

                request.addHeader("Accept-Language", "en_US, de");

             }

             @Override

             protected void onResponse(EnhancedMockHttpServletResponse response)

             {

                assert response.getStatus() == 200;

                assert response.getContentAsString().equals("foobar");

             }

          }.run();

       }

    }

このテストはローカル呼び出しのみを実行します。TCP を通した `SeamResourceServlet` とは連携しません。モック要求は Seam サーブレットとフィルタを通され、その結果、その応答はテストアサーションのために利用できます。 `ResourceRequestEnvironment` の共有インスタンスで `getDefaultHeaders()` メソッドをオーバーライドすることで、テストクラスのテストメソッド毎にレクエストヘッダーをセットすることができます。

`ResourceRequest` が `@Test` メソッドや `@BeforeMethod` コールバックの中で実行されなければならないことに留意してください。 `@BeforeClass` のような他のどのコールバックでも実行することはできません。

- 前のページ第23章 キャッシュ
- 上に戻る
- ホーム
- 次のページ第25章 リモーティング