木曜日 6 29, 2006
木曜日 6 29, 2006
前回のエントリでは、SJS AppServer 9(glassfish)を使用してのWebサービス・クライアントの作成方法を紹介しました。glassfishに含まれる(実は、Java SE 6 (Mustang)にも含まれています)wsimportコマンドにより生成されたクライアント用のスタブコードは、JAX-WS 2.0、JAXB 2.0のみに依存しており、AppServerベンダによらずポータビリティがあります。
ただし、これまで扱ってきたサンプルには1つ問題があります。wsimportコマンドがWSDLから生成したエンドポイント・インタフェースは、サーバ側のWebサービス本体のcom.example.Calcクラスと全く同じクラス名になってしまっています。
package com.example;
:
@WebService(name = "Calc", targetNamespace = "http://example.com/")
public interface Calc {
:
}
もし、wsimportコマンドを実行する時のカレントディレクトリをソースツリーのトップのディレクトリにして実行すると、Webサービス本体の実装クラスcom/example/Calc.javaを上記のインタフェースで上書きしてしまいます。
この問題を回避するためには、Webサービス本体のクラス名とエンドポイント・インタフェース名が違う名前になるように、@WebServiceアノーテーションをカスタマイズしてあげる必要があります。wsimportコマンドはWSDLファイルの記述を元にサービスクラス名やエンドポイント・インタフェース名を決定します。@WebServiceはWSDLの内容を制御するために定義するものであるため、サービスクラスやエンドポイント・インタフェースが期待する名前になるように、@WebServiceアノーテーションのメンバ(アノーテーションの()内に記述する属性のようなもの)の値を明示してあげることでこの問題を回避できます。@WebServiceのデフォルトの振舞は、Web Services Metadata (JSR-181)仕様の5.1.1節に説明がありますが、以下に抜粋しておきます。
| メンバ(要素)名 | WSDLとのマッピング | Javaでの意味 | デフォルト値 |
|---|---|---|---|
| name | wsdl:portTypeのname属性 | クライアント用SEIのインタフェース名に使用される | Javaクラスまたはインタフェース名(除くパッケージ名) |
| portName | wsdl:portのname属性 | Serviceクラスからポートを取得するファクトリメソッド名に使用される(ex. CalcService#getCalcPort()) | @WebService.name+"Port" |
| serviceName | wsdl:serviceのname属性 | javax.xml.ws.Serviceを実装したポートのファクトリクラス名に使用される | Javaクラスまたはインタフェース名(除くパッケージ名)+"Service" |
| targetNamespace | WSDLの各要素および、XMLスキーマ要素のネームスペース | @WebServiceを付与したクラスが含まれるパッケージ名をURLに変換したもの | |
| endpointInterface | wsdl:portTypeのname属性, targetNamespaceを決定する | エンドポイント・インタフェース名(含むパッケージ名) | なし |
| wsdlLocation | 予め用意されたWSDLが取得できるURL | なし |
ここでは、Webサービスの本体のクラス名をCalcImplとし、エンドポイント・インタフェース名をCalcとしたい(CalcImplという名前はクライアントには見せたくない)とします。このためには、Webサービス本体のクラス名をCalcからCalcImplに変更し、@WebServiceアノーテーションを以下のように変更するとよいでしょう。
package com.example;
import javax.jws.WebService;
@WebService(name="Calc", serviceName="CalcService")
public class CalcImpl {
public int add(int i, int j) {
return i + j;
}
}
個人的にはserviceName要素のデフォルトがクラス名+"Service"ではなく、@WebService.name+"Service"という仕様だったらよかったのにと思います。もしそうならportName要素と同じようにserviceName要素も省略することが可能でした。何はともあれ、このクラスをデプロイすると、Webサービス本体のクラス名を変えても、@WebService.name="Calc"を基点として、serviceName="CalcService"、portName="CalcPort"となった奇麗なWSDLが期待通りに生成され、wsimportコマンドで生成されるエンドポイント・インタフェースもCalc.javaになっていることが確認できると思います。
もし、EJBのリモート・インタフェースとWebサービスのインタフェースを共用したい、JPA(Java Persistence API)用のドメインモデル用クラスとWebサービスの入出力用クラスを共用したいなどの理由でCalcインタフェースおよび一連のPOJOクラスを予め用意しており、これを使用したい場合は、以下のようなインタフェースを予め用意しておけばよいでしょう。
package com.example;
import javax.jws.WebService;
@WebService
public interface Calc {
public int add(int i, int j);
}
この場合、wsimportコマンドがCalcインタフェースや入出力用のPOJOのクラスを上書きしてしまわないように注意が必要です。なお、Webサービス本体のCalcImplクラスは必ずしもCalcインタフェースをimplementsする必要はないことに注意してください。
本来、既存のインタフェースをエンドポイント・インタフェースとして、使用する場合には、Webサービスの実装クラス側で@WebService.endpointInterface要素にエンドポイント・インタフェースを指定するのがよいのですが、現在のところこれはお薦めできません。Web Service Metadata 1.0 (JSR-181) FR版の3.1節 pp.13によると、「Webサービス実装クラスの@WebServiceにendpointInterface要素を指定した場合、serviceName要素以外は指定できない」とあります。
The implementation bean MAY reference a service endpoint interface by using
the @WebService.endpointInterface annotation. If the implementation bean
references a service endpoint interface, that service endpoint interface is used to
determine the abstract WSDL contract (portType and bindings). In this case, the
service implementation bean MUST NOT include any JSR-181 annotations other
than @WebService(endpointInterface and serviceName attributes only),
@HandlerChain and @SOAPMessageHandlers.. More information on the
@WebService.endpointInterface attribute may be found in 4.1 Annotation:
javax.jws.WebService.
そうすると、name要素の値はendpointInterface要素から"Calc"と決定されるのですが、portName要素はクラス名のデフォルト"CalcImpl"になってしまいます。これにより、CalcServiceクラスからCalcポートを取得するためのメソッド名がgetCalcImplPort()となってしまいます。endpointInterfaceを指定するとportNameが制御できないのは仕様としてよろしくありません。実際、Web Service MetadataにはV2.0のメンテナンス・リリースの仕様がつい先日出されており、上記の部分は以下のように「Webサービス実装クラスの@WebServiceでendpointInterfaceを指定した場合、name要素を含んではいけない」と修正されました。
The implementation bean MAY reference a service endpoint interface by using
the @WebService.endpointInterface annotation. If the implementation bean
references a service endpoint interface, it MUST implement all the methods on
the service endpoint interface. If the implementation bean references a service
endpoint interface, that service endpoint interface is used to determine the abstract
WSDL contract (portType and bindings). In this case, the service implementation
bean MUST NOT include any JSR-181 annotations other than @WebService
and @HandlerChain. In addition, the @WebService annotation MUST NOT
include the name annotation element. More information on the
@WebService.endpointInterface annotation element may be found in 4.1
Annotation: javax.jws.WebService.
しかしながら、glassfishの実装が、Web Service Metadata V2.0仕様に合わせて修正されるのは当分先になるでしょう(本日時点の最新版であるV1 UR1 Build 02 21-June-06でも、V2 Build 08 21-June-06でも、V1.0仕様のままであることを確認しています)。現状の対処方法としては、予めインタフェースを用意する場合であってもendpointInerface要素は使用せず、name要素とserviceName要素の宣言で対処するのがよいようです。
火曜日 6 27, 2006
今回は、JAX-WS 2.0仕様に従ったWebサービス・クライアントのコーディングとその実行方法について御紹介します。
Webサービスの呼出し方法には、目的に応じて様々な方法が用意されていますが、ここではもっとも簡単なエンドポイント・インタフェースに基づくプロキシーを用いた同期呼び出しの方法を説明します。
まず、Java EE 5チュートリアル:Webサービス編でデプロイしたCalcサービスのWSDLから、クライアント用のプロキシー(スタブ)コードを生成します。SJS AppServer 9(glassfish)ではこのためのコマンドラインツールとしてwsimportコマンドが用意されていますので、wsimportコマンドの引数にWSDLのURLを指定して以下のように実行します。
$ mkdir work $ cd work $ $ASROOT/bin/wsimport -keep http://localhost:8080/Calc/CalcService?WSDL
ここで、-keepオプションは、生成されたスタブコードのjavaソースも残すことを意味します。-keepオプションがないと、コンパイルされたクラスだけがカレントディレクトリに生成されます。
また、WSDLのURLは、前回のエントリで紹介した管理コンソールのWebサービスCalcのページで、「Test」ボタンを押した時に表示されるテストクライアントのページに(WSDL File)というリンクがありますので、これをブラウザ上で右クリックしてコピーするといいでしょう。wsimportコマンドで生成されるファイルは以下の通りです。
$ find . . ./com ./com/example ./com/example/Add.class ./com/example/Add.java ./com/example/AddResponse.class ./com/example/AddResponse.java ./com/example/Calc.class ./com/example/Calc.java ./com/example/CalcService.class ./com/example/CalcService.java ./com/example/ObjectFactory.class ./com/example/ObjectFactory.java ./com/example/package-info.class ./com/example/package-info.java
Webサービスのメソッドを呼び出す手順は、ポートにマッピングされるエンドポイント・インタフェース(ここではCalcインタフェース)のスタブを取得し、そのインタフェースに定義された目的のメソッド(ここではadd()メソッド)を呼び出すということになります。
それでは、エンドポイント・インタフェースのスタブはどのようにして取得すればいいでしょうか。できればカッコよくリソース・インジェクションを使いたいものです。クライアント・オブジェクトにWebサービス・スタブをインジェクションするには、@WebServiceRefアノーテーションを使用します。以下が、@WebServiceRefを使用したクライアントコードの例です。
import com.example.Calc;
import com.example.CalcService;
import javax.xml.ws.WebServiceRef;
public class Client {
@WebServiceRef
private static CalcService service;
public static void main(String args[]) {
Calc port = service.getCalcPort();
int i = 10;
int j = 20;
System.out.printf("%d + %d = %d%n", i, j, port.add(i, j));
}
}
上記の例では、staticなメンバ変数serviceにCalcサービスがインジェクションされることになります(インジェクションされるのは、クラスがインスタンス化されるときの1回だけですので、インジェクションされるメンバ変数は必ずstaticで宣言します)。CalcServiceクラスには、Calcポートのプロキシーを取得するためのgetCalcPort()メソッドが用意されているためこれを利用して、Calcポートのプロキシーを取得します。プロキシーが取得できたら、後は目的のメソッド(ここではadd(int,int)メソッド)を呼び出すだけです。
また、@WebServiceRefアノーテーションは、Calcポートのプロキシーを直接クライアントのクラスにインジェクションすることもできます。この場合は、@WebServiceRefの属性にそのポートを取得するためのサービスクラス(この場合、CalcServiceクラス)を教えてあげる必要があります。
import com.example.Calc;
import com.example.CalcService;
import javax.xml.ws.WebServiceRef;
public class Client {
@WebServiceRef(CalcService.class)
private static Calc port;
public static void main(String args[]) {
int i = 10;
int j = 20;
System.out.printf("%d + %d = %d%n", i, j, port.add(i, j));
}
}
それでは、このクライアントをコンパイルして実行してみましょう。コンパイルするときには、Java EE 5のAPIが含まれるjavaee.jarをクラスパスに含めます。
$ javac -classpath $ASROOT/lib/javaee.jar:. Client.java
次にクライアントを実行しますが、上記のように@WebServiceRefアノーテーションを使用している場合には、単純にjavaコマンドを使用したのでは正しく実行されません。実は、スタンドアロンクライアントでのリソースインジェクションの機能を使いたい場合は、クライアントをJava EEクライアント・コンテナから起動する必要があります。そうすることで、クライアント・コンテナがリソースインジェクションのアノーテーションを検出し、適切なリソースをインジェクションしてくれるわけです。
J2EE 1.4の頃は、クライアントコンテナを利用するためには、クライアント用のデプロイメント記述子application-client.xmlを記述して、クライアントEARを作成する必要がありました。しかし、Java EE 5では、application-client.xmlに記述すべき<service-ref>に対応する@WebServiceRefがクラスに埋め込まれているため、わざわざapplication-client.xmlを用意する必要はありません。また、SJS AppServer 9(glassfish)では、必ずしもクライアントEARを作成する必要もありません。以下のように、コンパイル済みのクラス名を引数にして、javaコマンドの代わりにappclientコマンドでクライアントを実行するだけです。
$ $ASROOT/bin/appclient Client 10 + 20 = 30
ただし、実際にアプリケーション開発では、クライアント・コンテナを使用しないことが多いと思います。クライアント・コンテナを実行するコマンドは標準化されていないため、ベンダによって異なります。一般に、特定のアプリケーション・サーバー・ベンダのコマンドに依存したクライアント・プログラムのパッケージングはさけた方がいいでしょう。
この問題を避けるには、スタンドアロン・アプリケーションでは@WebServiceRefを使用するのを避け、以下のようなコードを使用するのがいいでしょう。
import com.example.Calc;
import com.example.CalcService;
public class Client {
public static void main(String args[]) {
CalcService service = new CalcService();
Calc port = service.getCalcPort();
int i = 10;
int j = 20;
System.out.printf("%d + %d = %d%n", i, j, port.add(i, j));
}
}
サーバがlocalhostではなく、リモートのホストである場合は、WSDLのURLとサービスのQNameを明示的に指定するもう一つのコンストラクタを使用すればOKです。
URL wsdlLocation = new URL("http://remote.host:8080/Calc/CalcService?WSDL");
QName serviceName = new QName("http://example.com/", "CalcService");
CalcService service = new CalcService(wsdlLocation, serviceName);
今度は、普通にjavaコマンドを使って実行することができます。ただし、クラスパスには、Java EE 5 APIのjavaee.jarとJAX-WSランタイムのappserv-ws.jarをクラスパスに入れる必要があります。
$ java -classpath $ASROOT/lib/javaee.jar:$ASROOT/lib/appserv-ws.jar:. Client 10 + 20 = 30
また、別の実行方法としては、Java SE 6 (Mustang)を利用することも考えられます。MustangにはJAX-WS 2.0のランタイムが含まれていますので、上記のクラスパス設定は不要になります。
$ java -version java version "1.6.0-dp" Java(TM) SE Runtime Environment (build 1.6.0-dp-b82-11) Java HotSpot(TM) Client VM (build 1.6.0-b82-6-release, mixed mode, sharing) $ java Client 10 + 20 = 30
クライアントのコードは、wsimportコマンドにより生成された一連のクラス群に依存していますが、ポータビリティに関しては全く心配がありません。生成されたクラスのソースを見て頂ければ分かりますが、これらは、JAX-WS 2.0およびJAXB 2.0の標準APIのみに依存しています。そのため、JDK 1.5 + Java EE 5、またはJDK 1.6の環境であれば、ベンダに関わらず、上記のWebサービスクライアントは実行可能です。
月曜日 6 26, 2006
先月のJavaOne SF開催中および開催後暫くは、ここからプレゼンテーションスライドが参照できたのですが、すぐにPDFへのリンクがはずされてしまっていました。
しかし、また、java.sun.comのJavaOneページからダウンロードできるようになっています。JavaOneのページからSessionsのリンクを辿って、"Download the Technical Session Presentation Files Now"の"Click here"のリンクを開きます。
ダイレクトリンクは、こちらになります。
木曜日 6 22, 2006
前回のエントリで、クラス1つのWebサービスならそのクラスをSun Java System AppServer 9(glassfish)のautodeployディレクトリにコピーするだけでWebサービスとして配備し、AppServerに付属のテストクライアントを使って簡単に動作確認できることを紹介しました。
しかし、実際のアプリケーション開発では、サービス・オブジェクトの入出力が単純なプリミティブであることはまずあり得ません。サービス・オブジェクトには複雑なオブジェクトツリーを与えて、加工されたオブジェクト・ツリーを得るのが一般的です。例えば、WebサービスCalcがcalc()メソッドの戻り値として、Resultオブジェクトを返す場合を考えてみましょう。
@WebService
public class Calc {
public Result calc(int i, int j) {
Result ret = new Result();
ret.sum = i + j;
ret.diff = i - j;
retun ret;
}
}
public class Result {
public int sum;
public int diff;
}
このWebサービス・モジュールが必要とするクラスは、CalcクラスとResultクラスの2つになります。このような場合は、Webサービスの実行に必要な複数のクラスをWARファイルかEJB JARファイルとしてまとめて、デプロイすることになります。
WebサービスをWARにまとめる場合であっても、必要なクラスをjarコマンドでアーカイブするだけです。webservices.xmlを始めとするDDファイルはおろか、web.xmlへの<servlet>タグの定義も必要ありません。WARファイルの作成手順は以下のような感じです。今回もNetBeansには頼らず、コマンドラインだけで作業してみましょう。
$ ls com/example/ ./ ../ Calc.java Result.java $ mkdir -p WEB-INF/classes/ $ javac com/example/*.java -d WEB-INF/classes/ $ jar cvf calc.war WEB-INF/classes/com/example/*.class
出来上がったWARファイルは、以下のようにコンパイルされたクラスだけが入ったものです。
$ jar tf calc.war META-INF/ META-INF/MANIFEST.MF WEB-INF/classes/com/example/Calc.class WEB-INF/classes/com/example/Result.class
後は出来上がったcalc.warをautodeployディレクトリにコピーすればWebサービスはServletとしてデプロイされます。
$ cp calc.war $ASROOT/domains/domain1/autodeploy/ ※ASROOTはAppServerのインストールルートのディレクトリ
また、上記のようにWebサービスをServletとしてデプロイする代わりに、EJBとしてデプロイする方法もあります。この場合、サービス・オブジェクトをWebサービスとEJBのStateless Session Beanのハイブリッド形式にするわけです。といっても、難しいことはなにもありません。単にStateless Session Beanを表す@Statelessアノーテーションをクラスに追加するだけです。
package com.example;
import javax.jws.WebService;
import javax.ejb.Stateless;
@WebService
@Stateless
public class Calc {
public Result add(int i, int j) {
return new Result(i + j);
}
}
後はEJB JARファイルを作成するわけですが、従来は必須であったEJB用のデプロイメント記述子ejb-jar.xmlの定義は必須ではなくなりました。この例では単に必要なクラスをjarコマンドでアーカイブするだけになります。
$ javac com/example/*.java $ jar cvf calc.jar com/example/*.class
出来上がったEJB JARファイルは以下の通りです。
$ jar tf calc.jar META-INF/ META-INF/MANIFEST.MF com/example/Calc.class com/example/Result.class
デプロイメントは、WARの時と同様、autodeployディレクトリにcalc.jarをコピーするだけです。先ほどのcalc.warをアンデプロイしてから、calc.jarをデプロイします。
$ rm $ASROOT/domains/domain1/autodeploy/calc.war $ cp calc.jar $ASROOT/domains/domain1/autodeploy/
管理コンソールで確認すると、WebサービスはServletではなく、今度はEJBとしてデプロイされていることが分かります。
WebサービスをServletとしてデプロイするか、EJBとしてデプロイするかに関わらず、サービスオブジェクトのコーディングスタイルは一貫してPOJOであり、わずかなアノーテーションによって、そのサービスへのランタイム環境でのアクセスプロトコルを自由に変更できることは素晴らしいことです。
それでは、WebサービスはServlet形式、EJB形式のどちらでデプロイするのが好ましいでしょうか?その答えは、そのサービス・オブジェクトがどのような処理をするかによるでしょう。もし、サービス・オブジェクトの中にデータベース・アクセスなどトランザクション処理があるのであれば、迷わずEJB形式をお勧めします。他にもEJB形式にすると、Spring FrameworkやSeasar2でお馴染みのリソースのDI(Dependency Injection)、インターセプターによるAOP、ロールベースのアクセス制御などの付加機能をソースのロジックに手を入れることなく、アノーテーションによるメタデータ宣言で制御できるメリットもあります。そして、それらの機能は全てJava EE 5標準仕様として提供されているのです。
一方、サービスがファイルアクセスしかないシンプルなものでよいのであれば、Servlet形式でもよいでしょう。いずれにしても、Servlet形式からEJB形式への変更、あるいはその逆は上記のようにとても簡単なので、仮に開発プロジェクトの途中で変更することになっても何も恐いことはありません。
さて、ここで一つ問題が。AppServerに付属するテストクライアントですが、Webサービスのオペレーションの引数がプリミティブでない場合についてはうまく扱えないようです。例えば、Calc#add(int,int)メソッドを以下のようにCalc#add(Values)に変えてデプロイしてみましょう。
@WebService
public class Calc {
public int add(Values val) {
return val.i + val.j;
}
}
public class Values {
public Values() {}
public Values(int i, int j) {
this.i = i;
this.j = j;
}
public int i;
public int j;
}
テストクライアントのページは以下のようになり、何を入れてもNullPointerExceptionとなります。
どうやら、付属のテストクライアントは、プリミティブ型以外のパラメータには対応していないようです(glassfishのCVSソースツリーWebServiceTesterServlet.javaのconvertWebParam()参照)。この入力フィールドにOGNLかGroovy表現を入力することができれば問題ないのでしょうが、今のところこのようなエンハンスの予定はなさそうです。