本稿では、近年の業界動向に対するLiberty Coreの強みを紹介し、その上でLiberty Coreを利用してJAX-RSおよびCDIを使って小規模なWebサービスを作成する方法を解説する。
1 業界に浸透しつつある「マイクロサービス」
近年、エンタープライズシステムやWebアプリケーションに関わる人々の間で「マイクロサービス」と呼ばれるアーキテクチャスタイルが話題にのぼるようになってきている。マイクロサービスとは、サービス全体がより小規模な独立したサービスの組み合わせによって構成されるようなアーキテクチャの設計スタイルを指す用語であり、Martin Fowler氏とJames Lewis氏が公開した「Microservices」という記事の中で詳しい定義が行われて注目を集めるようになった。
マイクロサービスでは、それぞれのサービスが単一の独立したアプリケーションとして完結していることが重要なポイントとなる。サービス同士の依存性を低く保つ一方で、各サービスはHTTPのようなオープンでシンプルな通信プロトコルによって緩く結合する。これによって、従来のモノリシックなシステムに比較して次のようなメリットを享受できるとされている。
- システムを構成する各サービスを独立して開発・デプロイすることができる
- 各サービスを独立してスケールすることができる
- モジュールの境界が明確なため、開発時のテストや、問題発生時の影響範囲の特定が容易
- 複数のチームでの開発や運用が行いやすい
システム全体をより小さなサブシステムの集合として構成する考え方自体は特に新しいものではなく、従来のSOA(Service-Oriented Architecture)などもその一種といえる。ただし、SOAの場合はサブシステムごとに個別に実装するというスタイルだったのに対して、マイクロサービスは機能単位にサービス化するという点で、その粒度や可搬性が異なっている。マイクロサービスとして提唱されるスタイルは、クラウドサービスに求められる要件との相性が良いことから、今後も必要性が高まってくるものと考えられる。
2 マイクロサービスとの相性もバッチリな「WAS Liberty Core」
実装の観点からマイクロサービスを考えた場合、独立性を保った小さなWebサービスを、いかに素早く開発し、市場に投入していけるかが重要なポイントとなる。そのためには軽量で手軽に使える開発およびテスト環境が必要だ。WAS Liberty Coreは、まさしくその「軽量で起動が速く、効率のよい開発を実現する」という目的のために生み出されたアプリケーションサーバーであり、マイクロサービスのようなアーキテクチャスタイルとも極めて相性が良い製品ということができる。
特に最新のLiberty CoreはJava EE 7に正式対応し、「JAX-RS 2.0」や「CDI 1.2(注1)」といった魅力的な機能がデフォルトで使えるようになっている。これらの機能は他のWebサービスと"緩く"連携する上で極めて重要な要素である。
Java EE 7リリース時のCDIのバージョンは1.1だったが、2014年4月にマイナーアップデートされて現在の最新版は1.2になっており、LibertyにもCDI 1.2の実装が同梱されている。
ここで、Liberty Coreについておさらいしておこう。Liberty CoreはWAS v8.5から追加されたLibertyプロファイルをベースとして構成されている。今年6月にリリースされたLibertyプロファイルの最新版では、従来の「Java EE 6 Web Profile」に加えて、「Java EE 7 Web Profile」および「Java EE 7 Full Profile」もサポートに加わった。つまりLibertyプロファイル自身は、Web Profileにとどまらない、Javaエンタープライズアプリケーション全域をカバーできる存在となっている。
Liberty Coreで利用可能な機能はLibertyプロファイル機能のサブセットであり、これらのうち「Java EE 6 Web Profile」および「Java EE 7 Web Profile」に準拠している。Liberty CoreとWASの他のエディションとのより具体的な機能面での違いについては、Liberty Coreのオンラインマニュアルのフィーチャー一覧を参照していただきたい。
さて、Webアプリケーションに関連したJava EE 7の新機能としては、次のようなものが挙げられる。
- JAX-RS 2.0(JSR 339: JAX-RS 2.0: The Java API for RESTful Web Services)
- CDI 1.1(JSR 346: Contexts and Dependency Injection for Java EE)
- JSON Processing(JSR 353: Java API for JSON Processing 1.0)
- WebSocket(JSR 356: Java API for WebSocket 1.0)
今回はこのうちのJAX-RSとCDI、JSON Processing(以下、JSON-P)の3つのAPIを活用して、JSONベースで他のサービスと連携可能な小さなWebサービスを構築してみる。なお、WebSocketを使用する方法については、『「WebSphere Application Server Liberty Core」で新たに正式サポートされたWebSocketを使ってみた』で詳しく解説している。
3 最新版Libertyプロファイルの導入
Libertyプロファイル向けの開発環境を用意するには、Eclipse用プラグイン「WebSphere Application Server Developer Tools(WDT)」を導入するのが一番手軽である。環境構築の方法については『軽量・高速・安価で、安定性と信頼性も兼ね備えたWebSphere Application ServerのLiberty Coreを使ってみた』で詳しく解説しているので、そちらも併せて参照していただきたい。
本稿ではEclipse 4.5(Mars) Java EE Developers(Pleiadesで日本語化済み)に対して、プラグイン「IBM WebSphere Application Server Liberty Profile Developer Tools for Mars(WDT)」を追加して利用した。EclipseへのWDTのインストールはマーケットプレースから行うことができる。[ヘルプ]-[Eclipseマーケットプレース]を選択してマーケットプレースを立ち上げ、「WebSphere Liberty」で検索をかければ対象のプラグインが見つかるはずだ(図3.1)。
プラグインをインストールできたら、Libertyプロファイルのサーバー定義を作成する。サーバー定義の作成手順もこれまでのバージョンとほぼ同様だ。まず新規サーバー定義のウィザードを立ち上げ、サーバータイプとしてLiberty Profile を選択する(図3.2)。
ランタイム環境は、今回はリポジトリから新規にインストールするので「Install from an archive or repository」にチェックを入れておく。また、JREは最新のJava 8を使用する(図3.3)。
ランタイムのインストール先は任意の場所を指定できる。リポジトリからインストールする場合には「Download and Install ...」にチェックを入れれば、図3.4のように利用可能なプロファイルが表示される。今回はJava EE 7のWeb Profileを使用するが、前述のように最新のLibertyではJava EE 7 Full PlatformやJava EE 6 Web Platformなども選択できる。
続いて追加プラグインの選択だが、今回使用したいCDIやJAX-RS、JSON-Pは標準でサポートされているので、追加のプラグインは特に必要ない(図3.5)。
プロファイル名には任意の名前を指定する(図3.6)。
サーバー定義が作成できたらサーバー構成を確認しておこう。サーバーマネージャーで「サーバー構成」をダブルクリックしてservers.xmlを開き(図3.7)、「フィーチャーマネージャー」を表示する。これまでのLibertyプロファイルでは使用したいフィーチャを1つずつ追加していく必要があったが、V8.5.5.6では「Web Profile」向けのフィーチャが用意されており、これを追加しておけばJava EE 7 Web Profileに含まれるフィーチャがすべて使えるようになる(図3.8)。もちろん、その他に必要なフィーチャを追加したり、不要なフィーチャを除外したりといった設定もこの画面で行える。
4 CDIとJAX-RSを使ったWebサービスの作成
今回は簡単なWebサービスとして、パラメータに7桁の郵便番号を渡すと、それに対応した住所を返すようなアプリケーションを実装してみる。実は『軽量・高速・安価で、安定性と信頼性も兼ね備えたWebSphere Application ServerのLiberty Coreを使ってみた』でも同様のサンプルを実装しているが、当時はJAX-RS 1.1とCDI 1.0の組み合わせだった。一方Java EE 7のWeb Profileでは、特に以下の点が従来と大きく異なっている。
- CDIを使う際にbeans.xmlの定義が省略できるようになった
- JAX-RSが正式にCDI連携に対応した
- JSON形式のデータをJSON-Pで簡単に扱えるようになった
- JAX-RSにクライアントAPIが追加された
JAX-RSとCDIの連携については、JAX-RS 1.1は直接CDIには対応していなかったため、JAX-RSのリソースクラスをEJB連携機能を使ってEJB化することで、間接的にCDIを利用していた。それに対してJAX-RS 2.0は正式にCDIをサポートするようになったため、EJBを経由することなく直接CDIを使えるようになっている。
では、早速実装していこう。プロジェクト名は「ZipcodeService」とした。
まず、住所を表すAddressクラスを次のように定義する。
public class Address { private String id; // ID private String zip; // 郵便番号 private String address; // 住所 public Address(String id, String zip, String address) { this.id = id; this.zip = zip; this.address = address; } public String toString() { return this.id + ":" + this.zip+ ":" + this.address; } // Setter/Getterは省略 }
住所データの一覧を扱うロジックは、AddressListクラスに次のように実装した。スコープは@ApplicationScopedを指定した。
@ApplicationScoped public class AddressList { private List<Address> addressList = new ArrayList<>(); // 住所のリストを格納 public AddressList() { try { // csvファイルから郵便番号と住所のリストを読み込む URI uri = this.getClass().getResource("postalcode.csv").toURI(); try (Stream<String> source = Files.lines(Paths.get(uri))) { this.addressList = source.map(s -> new Address(s.split(",")[0], s.split(",")[1], s.split(",")[2])) .collect(Collectors.toList()); } catch(IOException ex) { ex.printStackTrace(); } } catch (URISyntaxException e) { e.printStackTrace(); } } /** * 指定された郵便番号の住所データを検索して返す * @param zipcode 7桁の郵便番号 * @return マッチしたAddressを格納したList */ public List<Address> search(String zipcode) { List<Address> result = this.addressList.stream() .filter(s -> s.getZip().equals(zipcode)) .collect(Collectors.toList()); return result; } /** * すべての住所データを返す * @return すべてのAddressを格納したList */ public List<Address> getAll() { return this.addressList; } }
郵便番号データは日本郵便が公開している郵便番号データから、郵便番号と住所の情報のみ抽出し、一意のIDを付加したものをCSVファイルとして使用した。これをAddressListのコンストラクタで読み込んでListとして保持する。AddressListには、読み込んだ住所データから、指定された郵便番号の住所を検索して返すsearch()メソッドと、すべての住所データを返すgetAll()メソッドを用意している。
JAX-RSのリソースクラスは、SearchAddressクラスとして次のような実装になった。
@ApplicationScoped @Path("/address") public class SearchAddress { @Inject private AddressList addressList; // データソース /** * すべての住所データを取得し、JSON形式で返す */ @Path("all") @GET @Produces({MediaType.APPLICATION_JSON}) public List<Address> all() { List<Address> address = addressList.getAll(); return address; } /** * 指定された郵便番号の住所データを検索し、結果をJSON形式で返す */ @Path("search") @GET @Produces({MediaType.APPLICATION_JSON}) public List<Address> search( @QueryParam("zip") String zipcode) { List<Address> address = addressList.search(zipcode); return address; } /** * JSON形式で指定された郵便番号の住所データを検索し、結果をJSON形式で返す */ @Path("jsonsearch") @POST @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) public List<Address> jsonsearch(ZipRequestParam param) { List<Address> address = addressList.search(param.getZip()); return address; } }
ポイントは、データソースとなるAddressListオブジェクトをCDIを利用して@Injectアノテーションでインジェクションしていることだ。こうすることで、SearchAddressクラスはAddressListの実装への依存を取り除くことができる。
このクラスでは、Webサービスとして機能する3つのメソッドを用意している。1つはすべての住所データを取得するためのGETリクエストを提供するall()メソッドで、AddressListのgetAll()の実行結果をJSON形式で返す。リクエストパスは「/address/all」となるように設定している。レスポンスをJSON形式にするには、@Producesアノテーションを使って「@Produces({MediaType.APPLICATION_JSON})」を指定すればよい。
2つ目は、「zip」パラメータで指定した郵便番号をGETリクエストとして受け取るsearch()メソッドである。このメソッドへのリクエストパスは「/address/search」となっており、AddressListのsearch()の結果をJSON形式で返す。
3つ目は、JSON形式で指定されたパラメータをPOSTリクエストとして受け取るjsonsearch()メソッドで、リクエストパスは「/address/jsonsearch」とした。
JSON形式でパラメータを受け取る場合は、@Consumesアノテーションを使って「@Consumes({MediaType.APPLICATION_JSON})」を指定すればよい。この例では、次の形式のJSONデータを受け取ることができる。
{ "zip" : "整数7桁の郵便番号" }
このJSON形式のパラメータを扱うためのクラスをZipRequestParamとして次のように定義した。
public class ZipRequestParam { private String zip; // 郵便番号 public ZipRequestParam() {} public ZipRequestParam(String zip) { this.zip = zip; } // Setter/Getterは省略 }
なお、all()、search()、jsonsearch()が返す結果のJSONデータは、いずれも複数の住所データに対応した次の形式になるようにしてある。メソッドの処理としてはListオブジェクトをreturnしているだけだが、これをJAX-RSがJSONの配列として取り扱ってくれる。
[ { "address" : "住所1", "id" : "ID1", "zip" : "郵便番号1"}, { "address" : "住所2", "id" : "ID2", "zip" : "郵便番号2"}, ...... ]
JAR-RSを使用するために必要となるApplidationConfigクラスについては次のようにした。ルートパスを「ws」とし、リクエストを受け取るSearchAddressクラスを登録している。
@javax.ws.rs.ApplicationPath("ws") public class ApplicationConfig extends Application{ public Set<Class<?>> getClasses(){ HashSet<Class<?>> set = new HashSet<Class<?>>(); set.add(SearchAddress.class); return set; } }
5 ZipcodeServiceを使った住所の検索
一通りの実装が完了したら、プロジェクトをサーバー上で実行しよう。プロジェクトマネージャーから[実行]-[サーバーで実行]を選び、使用するサーバーとして「Liberty Profile」を指定する(図5.1)。
起動が完了したら、Webブラウザからリクエスト用のURLにアクセスしてみる。本稿の例では、すべての住所データを取得したい場合のURLは「http://localhost:9080/ZipcodeService/ws/address/all」であり、アクセスすると図5.2のようなJSONデータが返ってくるはずだ。
郵便番号を指定して検索する場合には、URLは「http://localhost:9080/ZipcodeService/ws/address/search?zip=郵便番号」となる。検索条件として「5220002」を指定した場合には、図5.3のようなJSONデータが返される。
続いて、JSON形式でのリクエストを試してみよう。今回は、リクエストを発行するクライアント側のHTML/JavaScriptとして次のようなコードを用意した。リクエストはAjaxによって発行し、レスポンスデータはテーブルで表示する。JavaScriptフレームワークはjQueryとDataTablesを利用している。DataTablesは、検索やソートなどの機能を持ったテーブルを簡単に構築することができるjQueryプラグインである。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>住所検索</title> <link rel="stylesheet" type="text/css" href="css/jquery.dataTables.min.css"> <script type="text/javascript" src="./js/jquery-2.1.4.min.js"></script> <script type="text/javascript" src="./js/jquery.dataTables.min.js"></script> <script type="text/javascript"> $(document).ready(function() { $('#dataTable').DataTable(); }); $(function () { $('#searchButton').click(function() { // テーブルの初期化 $('#dataTable').DataTable().destroy(); // リクエスト先のURL var target = "http://localhost:9080/ZipcodeService/ws/address/jsonsearch"; // リクエストパラメータ用のJSONデータ var paramData = { "zip" : $('#zipInput').val() }; // リクエストを発行してデータを取得し、テーブルを構成する $('#dataTable').DataTable( { "columns": [ { "data": "id", "sDefaultContent": "" }, { "data": "zip", "sDefaultContent": "" }, { "data": "address", "sDefaultContent": "" } ], "ajax": { "url": target, "type": "POST", "contentType": "application/json", "data": function () { return JSON.stringify( paramData ); } }, "sAjaxDataProp": "" }); }); }); </script> </head> <body> <h1 class='ui-widget-header'>住所検索</h1> <div style="margin:20px"> 郵便番号: <input id="zipInput" type="text" size="20px" /> <button id="searchButton" type="submit">検索</button> </div> <div style="margin:20px"> <table id="dataTable" class="display"> <thead align="left"> <tr> <th width="10%">ID</th> <th width="20%">郵便番号</th> <th>住所</th> </tr> </thead> <tbody> </tbody> </table> </div> </body> </html>
DataTablesでは、ajaxオプションを指定することで、jQueryと同様にAjax通信によってリクエストを発行し、その結果をテーブルのデータとしてセットすることができる。この例では、フォームに入力された文字列をzipパラメータの値とするようなJSONデータを作成し、それを使って「http://localhost:9080/ZipcodeService/ws/address/jsonsearch」に対してリクエストを発行している。
例えば郵便番号に「5220002」を入力して検索した場合、図5.5のような結果になった。
このときのリクエストおよびレスポンスのJSONデータは次のようなものになっていた。
{ "zip" : "5220002" }
[ { "address" : "滋賀県彦根市松原", "id" : "0075683", "zip" : "5220002" }, { "address" : "滋賀県彦根市松原町", "id" : "0075684", "zip" : "5220002"} ]
6 JAX-RS 2.0のクライアントAPIの利用
JAX-RS 2.0における注目すべき変更点の一つが、クライアント用のAPIが追加されたことである。これまでのJAX-RSではRESTful Webサービスのサーバー側の実装にしか対応しておらず、クライアント側は依然としてJava標準のHTTPクライアントAPIで実装するか、サードパーティ製の非標準のライブラリを利用するしかなかった。それに対してJAX-RS 2.0では、サーバーからクライアントまで一貫して標準APIだけで容易に実装できるようになったわけでだ。
そこで今度は、先ほど作成したZipcodeServiceに対して、JAX-RS 2.0のクライアントAPIを使ってリクエストを送ってみることにする。クライアントAPIを使ったリクエストの基本的な手順は次のようになる。
- Clientクラスのインスタンスを作成する
- リクエスト対象のURLを指定してWebTargetインスタンスを作成する
- リクエスト用のエンティティをEntityインスタンスとして作成する
- WebTargetからリクエストを処理するためのInvocation.Builderインスタンスを作成する
- Invocation.Builderを使ってリクエストを発行
以上を踏まえて、ZipcodeServiceのjsonserachを利用するスタンドアロンなJavaプログラムを次のように実装した。
public class JsonClientTest { public static void main(String[] args) { // リクエストターゲット Client client = ClientBuilder.newClient(); WebTarget target = client.target("http://localhost:9080/ZipcodeService/ws/address/jsonsearch"); String jsonText = "{\"zip\":\"5220002\"}"; // リクエスト用JSONデータの例 // Entityとレスポンスの型を指定してリクエストを発行 Entity<String> entity = Entity.entity(jsonText, MediaType.APPLICATION_JSON_TYPE); String response = target.request(MediaType.APPLICATION_JSON_TYPE).post(entity, String.class); System.out.println(response); } }
ClientインスタンスはClientBuilderクラスのnewClient()メソッドで作成する。そして作成したClientインスタンスに対してtarget()メソッドを呼び出すことでWebTargetインスタンスが取得できる。リクエスト用のEntityはstaticなentity()メソッドを使って作成する。このメソッドには第1引数にエンティティデータ本体を、第2引数にエンティティデータのタイプを指定すればよい。ここではJSONデータを指定している。
続いてWebTargetに対してrequest()メソッドを実行するとInvocation.Builderインスタンスが取得できるので、そこからpost()メソッドを実行することでリクエストを発行している。post()メソッドには引数としてEntityインスタンスと、レスポンスを受け取る型(この例ではString型)を指定する。
このプログラムを実行すると、次のようなJSONデータが返ってくるはずだ。
[ { "address" : "滋賀県彦根市松原", "id" : "0075683", "zip" : "5220002" }, { "address" : "滋賀県彦根市松原町", "id" : "0075684", "zip" : "5220002"} ]
なお、Java EE 7を使わずにJAX-RS 2.0を単体で利用したい場合には、このサイトから参照実装をダウンロードすることができる。
7 JSONを使ったWebサービス連携の試み
JAX-RS 2.0によってクライアントサイドとしてJSONデータを扱いやすくなったということは、Webサービスを構築する際に外部のRESTful Webサービスと連携させることも容易になったということでもある。外部のWebサービスから取得したJSONデータを、簡単に自前のWebサービスのデータソースとして利用できるからだ。
そこで次のサンプルでは、先ほどのZipcodeServiceと連携する新しいWebサービスを考えてみよう。今回作ろうとしているのは、7桁の郵便番号を指定すると、それに対応した住所に加えて、緯度・経度の値を返すサービスである。プロジェクト名は「ZipGeoService」とした。
まず、内部的に住所と緯度・経度をセットで扱うためのクラスをAddressGpsクラスとして次のように定義した。
public class AddressGps { private String zip; // 郵便番号 private String address; // 住所 private double[] location; // [緯度,経度] public AddressGps(String zip, String address, double[] location) { this.zip = zip; this.address = address; this.location = location; } // Setter/Getterは省略 }
クライアントから見た窓口となるJAX-RSのリソースクラスは、SearchDataというクラス名で次のように実装する。
@ApplicationScoped @Path("/geosearch") public class SearchData { @Inject private AddressGpsList addressList; // データソース /** * 指定された郵便番号の住所と緯度・経度データを検索し、結果をJSON形式で返す * レスポンスとして返されるJSONの形式: * [ {"address":"HOGEHOGE","location":[xxx,xxx],"zip":"xxxxxxx"}, * {"address":"PIYOPIYO","location":[xxx,xxx],"zip":"xxxxxxx"}, ... ] */ @Path("search") @GET @Produces({MediaType.APPLICATION_JSON}) public List<AddressGps> search( @QueryParam("zip") String zipcode) { List<AddressGps> address = addressList.search(zipcode); return address; } }
このクラスのsearch()メソッドが、郵便番号を「zip」パラメータで受け取り、検索結果をJSON形式で返すメソッドである。コンテキストパスは「/address/search」で、GETリクエストを受け付ける。
肝心のデータソースはAddressGpsListクラスとして定義しており、実装はCDIの@Injectアノテーションを利用してインジェクトしている。ZipcodeServiceのときと同様に、これでJAX-RSのフロントエンドと内部ロジックの依存性を取り除くことができる。
本稿の例ではAddressGpsListクラスの実装は次のようにした。
@ApplicationScoped public class AddressGpsList { final String baseurl = "http://localhost:9080/ZipcodeService/ws/address/"; public List<AddressGps> search(String zipcode) { List<AddressGps> result = new ArrayList<>(); // 結果格納用のList Client client = ClientBuilder.newClient(); // ZipcodeServiceに対してリクエストを発行 String request = "{ \"zip\":\"" + zipcode + "\" }"; // リクエスト用JSONデータ WebTarget target = client.target(this.baseurl + "jsonsearch"); Entity<String> entity = Entity.entity(request, MediaType.APPLICATION_JSON_TYPE); String response = target.request(MediaType.APPLICATION_JSON_TYPE).post(entity, String.class); // レスポンスのJSONデータからJsonObjectを作成 JsonReader jsonReader = Json.createReader(new StringReader(response)); JsonArray jsonArray = jsonReader.readArray(); List<JsonObject> list = jsonArray.getValuesAs(JsonObject.class); // Google Maps APIへのリクエストを発行 String googlemapUrl = "http://maps.googleapis.com/maps/api/geocode/json?address=" + zipcode + "&language=ja&sensor=false"; WebTarget mapTarget = client.target(googlemapUrl); String mapResponse = mapTarget.request(MediaType.APPLICATION_JSON_TYPE).get(String.class); // レスポンスのJSONデータを解析して、緯度・経度を取得 double[] location = getGeoLocation(mapResponse); // ZipcodeServiceからのレスポンスを1件ずつ処理する for(JsonObject obj: list) { // 郵便番号、住所、緯度・経度からAddressGpsを作成してListに格納 String address = obj.getString("address"); result.add(new AddressGps(zipcode, address, location)); } // 結果のListを返す return result; } private double[] getGeoLocation(String jsonString) { // JsonObjectを作成 JsonReader jsonReader = Json.createReader(new StringReader(jsonString)); JsonObject rootObj = jsonReader.readObject(); // "status"オブジェクトを取得し、値が"OK"の場合のみ処理する String status = rootObj.getJsonString("status").getString(); if(status.equals("OK")) { // "results"配列を取得 JsonArray resultsArray = rootObj.getJsonArray("results"); // "geometry"オブジェクト内の"location"オブジェクトを取得 JsonObject geometryObj = resultsArray.getJsonObject(0).getJsonObject("geometry").getJsonObject("location"); // 緯度・経度を取得 double lat = geometryObj.getJsonNumber("lat").doubleValue(); // 緯度 double lng = geometryObj.getJsonNumber("lng").doubleValue(); // 経度 // 戻り値の配列を作成して返す double[] location = {lat, lng}; return location; } else { // "status"がOK以外の場合はnullを返す return null; } } }
データの取得元は2か所あり、1つはZipcodeServiceで、もう一つはGoogle Maps APIである。前者からは住所を取得し、後者からは緯度・経度を取得し、それらを合わせてAddressGpsオブジェクトを作りレスポンスとして返すようにしている。
まずZipcodeServiceに対しては、/address/jsonsearchに対して郵便番号をJSON形式で渡し、返ってきたJSONデータから住所の部分を取り出している。JSONデータの解析にはJSON-PのオブジェクトモデルAPI(javax.json)を利用している。なお、この例ではJSONを使ったPOSTリクエストを行っているが、GETリクエストでも同様の結果を得ることができる。
一方、Google Maps APIについては、「Geocoding API」を使うことで郵便番号から対応する各種位置情報を取得することができる。具体的には、「address」パラメータに指定してGETリクエストを送ればいい。実は住所の情報はGoogle Maps APIからも取得することができるが、今回は複数のRESTfulサービスを組み合わせるサンプルということで、緯度・経度の情報のみりようするこのAPIではリクエストパスとして「http://maps.googleapis.com/maps/api/geocode/json」を使うことで結果をJSON形式で受け取ることができる。レスポンスデータなどの詳細はGoogle Maps Geocoding APIのサイトを参照していただきたい。緯度・経度の値は、locationオブジェクトのlatプロパティおよびlngプロパティにセットされているので、これをJSON-P APIを使って取り出せばよい。
最後にApplidationConfigクラスについては、リクエストを受け取るのがSearchDataクラスなので次のようなコードになった。
@javax.ws.rs.ApplicationPath("ws") public class ApplicationConfig extends Application{ public Set<Class<?>> getClasses(){ HashSet<Class<?>> set = new HashSet<Class<?>>(); set.add(SearchData.class); return set; } }
以上が完成したら、ZipcodeServiceとZipGeoServiceの2つのプロジェクトをそれぞれLiberty Profile上で実行し、Webブラウザから「http://localhost:9080/ZipGeoService/ws/geosearch/search?zip=郵便番号」のURLにアクセスしてみよう。例えば郵便番号の部分に「5220002」を指定した場合には、図7.5のような結果が返ってくるはずだ。
8 まとめ
今回は最新版のLibertyプロファイルおよびLiberty Coreで新たに正式サポートされたJAX-RS 2.0、CDI 1.1、そしてJSON-Pを用いて極めて簡単なRESTful Webサービスを実装してみた。JAX-RS 2.0でサーバーサイドだけでなくクライアントサイドのAPIが充実し、Java単体でのWebサービス間連携が非常に簡単に実装できるようになった。これはデータソースやビジネスロジックとして外部のサービスを手軽に利用できるようになったことを意味している。さらに、そこにCDIを組み合わせれば、フロントエンドと内部ロジックの切り分けが容易になり、アプリケーション全体の柔軟性が大きく向上することになる。
Liberty Coreの場合、Java EE 7のWeb Profileに準拠しているためこれらの機能を追加のAPIやプラグインなしに利用できるのが強みである。サーバーのフィーチャーとしてWeb Profileが一括で選択できるなどの修正も地味だがうれしい。また、LibertyプロファイルがJava EE 7 Full Profileにも対応したことから、Web Profileに収まらない機能強化が必要になった場合でも、よりシームレスに移行できるという強みも加わった。
冒頭でも紹介したように、今後は「大きなシステムを小さなサービスの組み合わせで構成する」という考え方がより重要になってくるだろう。開発のスピードに重点を置いたLiberty Coreはそのような時流に乗るための要に成り得る存在と言っていいだろう。
本稿に関連する詳細な技術資料を期間限定で無償提供しています。ぜひご活用ください。