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はそのような時流に乗るための要に成り得る存在と言っていいだろう。
本稿に関連する詳細な技術資料を期間限定で無償提供しています。ぜひご活用ください。