SeamFramework.orgCommunity Documentation

第24章 Webサービス

24.1. 設定とパッケージング
24.2. 対話型Webサービス
24.2.1. 推奨される方法
24.3. Webサービスの例
24.4. RESTEasy によるRESTful HTTP Webサービス
24.4.1. RESTEasy の設定と要求
24.4.2. Seam コンポーネントとしてのリソースとプロバイダ
24.4.3. セキュリティリソース
24.4.4. HTTP 応答に対する例外のマッピング
24.4.5. リソースとプロバイダのテスト

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

Webサービス要求をSeamがインタセプトできるように、必要なSeamのコンテキストがその要求に合わせて生成できなければなりませんが、それにはSOAPハンドラを特別に設定する必要があります。org.jboss.seam.webservice.SOAPRequestHandlerSOAPHandler実装として、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
>

では、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要素が含まれています。

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コンポーネントに委譲することで、長い対話を続けていることが判ります。

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

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

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

一例として、以下のリソース定義では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/plainde-DEをマップすることができます。

どのリソースやプロバイダのインスタンスも、デフォルトで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だけです!

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
>

リソースメソッドにより投げられた未チェックの UnsupportedOperationException501 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 エラーページにマッピングされるでしょう。

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 のような他のどのコールバックでも実行することはできません。