SeamFramework.orgCommunity Documentation
SeamとJBossWSを統合することで、標準のJEE のWebサービスにたいして、対話型Webサービスにも対応したSeamのコンテキストフレームワークを十分に活用することができます。本章では、Seam環境でWebサービスが動作するのに必要な手順を説明します。
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
>
では、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 ブートストラップと設定をシームレスに組み込み、リソースとプロバイダを自動検出。
SeamResourceServlet によりHTTP/REST 要求を処理し、外部サーブレットやweb.xml での設定は不要。
Seam コンポーネントとしてリソースを記述することで、Seamのライフサイクル管理とインタセプション(バイジェクション)をすべて利用可能。
まず始めに、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
をマップすることができます。
どのリソースやプロバイダのインスタンスも、デフォルトで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だけです!
components.xml
で Seam 認証フィルタを通して HTTP ベーシック認証とダイジェスト認証を有効にすることができます。
<web:authentication-filter url-pattern="/seam/resource/rest/*" auth-type="basic"/>
認証ルーチンの記述の仕方については Seam セキュリティの章を見て下さい。
認証が成功したら、@Restrict
アノテーションと @PermissionCheck
アノテーションを使った認可のルールが有効になります。クライアントの Identity
や権限のマッピングなどにもアクセスできるようになります。認可に関する Seam のセキュリティの機能のすべてが利用可能になるわけです。
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 エラーページにマッピングされるでしょう。
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
のような他のどのコールバックでも実行することはできません。