Ch8 : Play Api & Call
[Play Api & Call]
這章節我們會練習寫RestFul Api跟怎樣去呼叫別人寫好的Api,一般為了資料可以方便傳輸到Android或者iOS行動裝置上,通常會使用JSON格式來傳輸資料,以下示範,怎樣去實作這些功能。建議可以使用瀏覽器的Rest套件工具,來輔助您的測試。這裡我是使用到FireFox的附加元件RestClient來測試我寫好的API。
如果不太懂JSON或RestFul是什麼,可以參考Refernce介紹後,再開始撰寫就會有感覺了。
專案參考 : https://github.com/loveu8/playMyBtaisMariaDB/tree/Ch8
Work Hard, Play Hard!
[Api]
我們在app/controller新增ㄧ隻ApiController來開始練習。
package controllers;
import play.mvc.Controller;
public class ApiController extends Controller{
}
API - GET without queryString
使用Http GET方法去觸發我們寫好的Api,把要傳輸的Java物件轉換成JSON格式回覆,這邊我們建立遊戲角色基本設定的物件來練習。我們來建立一個角色是GM,回傳GM的資料
app/controller/pojo/game
新增Player.java類別,裡面有基本屬性,姓名,性別與種族。
package pojo.game;
public class Player {
// 遊戲角色姓名
private String name;
// 性別
private String sex;
// 種族
private String race;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getRace() {
return race;
}
public void setRace(String race) {
this.race = race;
}
}
app/controller/ApiController.java
// 取得遊戲管理者角色資料
public Result getGameMasterData(){
Player player = new Player();
player.setName("GM");
player.setSex("Male");
player.setRace("human");
response().setHeader("Content-Type", "application/json");
return ok (Json.toJson(player));
}
conf/routes
# http://127.0.0.1:9000/api/getGameMasterData
GET /api/getGameMasterData controllers.ApiController.getGameMasterData()
Request
GET : http://127.0.0.1:9000/api/getGameMasterData
Response
我們把回覆結果快速看過一次,若是成功呼叫的話會回傳status code:200的正常連線回傳訊息,我們注意到Content-Type的Header資訊,是寫使用application/json;charset=utf-8格式輸出,而本身Play在retrun Json格式資料時,會自動帶這些Header資訊,我們就不需要特別再去撰寫Response的資訊了。而回傳的Body,就是我們要的Json資料。
回覆的Header資訊
Status Code: 200 OK
Content-Length: 41
Content-Type: application/json;charset=utf-8
Date: Thu, 18 Aug 2016 09:17:11 GMT
X-ExampleFilter: foo
回覆的body資料
{
"name": "GM",
"sex": "Male",
"race": "human"
}
API - GET with queryString
我們來新增一個Pokemon的類別,使用神奇寶貝名字,來查詢出神奇寶貝的資訊吧。
app/controller/pojo/game
新增一隻Pokemon類別,建立一些基本常見的屬性。之後再新增一個PokemonDB列舉,裡面有皮卡丘,傑尼龜,小火龍與妙花種子的神奇寶貝。
package pojo.game;
public class Pokemon {
// 怪獸名
private String name;
// 生命點數
private int hp;
// 技能
private String skill;
// 等級
private String lv;
// 屬性
private String type;
public Pokemon(String name, int hp , String skill , String lv ,String type) {
this.name = name;
this.hp = hp;
this.skill = skill;
this.lv = lv;
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getHp() {
return hp;
}
public void setHp(int hp) {
this.hp = hp;
}
public String getSkill() {
return skill;
}
public void setSkill(String skill) {
this.skill = skill;
}
public String getLv() {
return lv;
}
public void setLv(String lv) {
this.lv = lv;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
app/controller/pojo/game
package pojo.game;
public enum PokemonDB {
Pikachu(new Pokemon("皮卡丘",50,"打雷","5","電系")) ,
Bulbasaur(new Pokemon("傑尼龜",60,"水炮","5","水系")) ,
Charmander(new Pokemon("小火龍",40,"噴射火焰","5","火系")) ,
Squirtle(new Pokemon("妙花種子",55,"飛葉快刀","5","草系"))
;
Pokemon pokemon;
PokemonDB(Pokemon pokemon){
this.pokemon = pokemon;
}
// 找出神奇寶貝
public static Pokemon findPokemon(String pokemonName) {
for (PokemonDB pokemon : PokemonDB.values()) {
if(pokemon.toString().equals(pokemonName)){
return pokemon.valueOf(pokemonName).getPokemon();
}
}
return null;
}
}
app/controller/ApiController.java
// 取得神奇寶貝基本資料
public Result findPokemon(String pokemonName){
// 利用神奇寶貝姓名,找出對應的怪獸資料
if(PokemonDB.findPokemon(pokemonName) != null){
return ok (Json.toJson(PokemonDB.valueOf(pokemonName).getPokemon()));
} else{
return ok (Json.toJson("查無神奇寶貝資料"));
}
}
conf/routes
# http://127.0.0.1:9000/api/findPokemon?pokemonName=Pikachu
GET /api/findPokemon controllers.ApiController.findPokemon(pokemonName : String)
Request
GET : http://127.0.0.1:9000/api/findPokemon?pokemonName=Pikachu
Response
有找到神奇寶貝。
{
"name": "皮卡丘",
"hp": 50,
"skill": "打雷",
"lv": "5",
"type": "電系"
}
Request
http://127.0.0.1:9000/api/findPokemon?pokemonName=XXX
Response
沒有找到神奇寶貝。
"查無神奇寶貝資料"
API - POST with JSON Data
上個範例,我們查詢了一筆神奇寶貝的資料,這次我們用多筆神奇寶貝的姓名來查詢多筆資料,呼叫Api後,會回傳一個List結果來告訴我們有沒有找到相關的神奇寶貝。
app/controller/ApiController.java
// 根據JSON傳過來要查詢的神奇寶貝姓名,回傳查詢結果
public Result findPokemons(){
// 把傳過的Body資料,轉換成Json格式
JsonNode requestJson = request().body().asJson();
// 預定要找的神奇寶貝資料
List<String> pokemonNames = new ArrayList<String>();
// 我們的Json格式 key 是 pokemonNames , 裡面儲存的是一個陣列
if(requestJson.get("pokemonNames").isArray()){
// 把所有要找的神奇寶貝,放到List pokemonNames裡
for (JsonNode objNode : requestJson.get("pokemonNames")) {
pokemonNames.add(objNode.asText());
}
}
play.Logger.info("Request json data = " + Json.toJson(pokemonNames));
//開始尋找對映的神奇寶貝,把結果放到 pokemonResult
List<Object> pokemonResult = new ArrayList<Object>();
for(int index = 0 ; index < pokemonNames.size() ; index ++){
if(PokemonDB.findPokemon(pokemonNames.get(index)) != null){
pokemonResult.add(PokemonDB.valueOf(pokemonNames.get(index)).getPokemon());
} else {
pokemonResult.add("查無神奇寶貝資料");
}
}
play.Logger.info("Response json data = " + Json.toJson(pokemonResult));
// 回傳查詢結果
return ok(Json.toJson(pokemonResult));
}
conf/routes
# http://127.0.0.1:9000/api/findPokemons
POST /api/findPokemons controllers.ApiController.findPokemons()
Request
URL : http://127.0.0.1:9000/api/findPokemons
Method : POST
Header : Content-Type : application/json
Body : {
"pokemonNames":
[
"Pikachu" ,
"Bulbasaur",
"XXXX",
"Charmander",
"Squirtle"
]
}
Response
我們對Api發動請求後,我們就可以把需要的神奇寶貝資料給撈取出來,我也故意塞入一個沒在資料裡的怪獸名,也ㄧ樣回傳查無資料的訊息,以下是呼叫後的回傳結果。
[
{
"name": "皮卡丘",
"hp": 50,
"skill": "打雷",
"lv": "5",
"type": "電系"
},
{
"name": "傑尼龜",
"hp": 60,
"skill": "水炮",
"lv": "5",
"type": "水系"
},
"查無神奇寶貝資料",
{
"name": "小火龍",
"hp": 40,
"skill": "噴射火焰",
"lv": "5",
"type": "火系"
},
{
"name": "妙花種子",
"hp": 55,
"skill": "飛葉快刀",
"lv": "5",
"type": "草系"
}
]
[Call]
當系統日漸龐大時,我們會希望把專案分開來,同時進行開發,很多時候,會需要呼叫別人寫好的Api,來達成我們需要的結果或畫面,以下會示範GET與POST怎樣從Play去呼叫別人寫好的Api,我也會利用我們剛剛寫好的查詢神奇寶貝Api範例,來當作練習。
首先我們要先在build.sbt新增javaWs到裡面,讓專案可以藉由Play ws呼叫別人的服務。
libraryDependencies ++= Seq(
javaWs
)
跟新增一隻CallController.java來去呼叫別人的Api。
package controllers;
import play.mvc.*;
import play.libs.ws.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import play.mvc.Controller;
public class CallController extends Controller{
@Inject WSClient ws;
}
Call - GET without queryString
我們嘗試呼叫Api取得Game master資料看看。要特別注意的就是,當我們去呼叫別人的Api時,請務必要包含在try catch範圍內,因為我們不可預期對方的Api可能會有異常狀況,而非常容易導致我們的服務也跟著錯誤,請務必注意。
app/controller/CallController.java
package controllers;
import play.mvc.*;
import play.libs.ws.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import play.mvc.Controller;
public class CallController extends Controller{
// 相依性注入 play.libs.ws.WSClient,可以用來呼叫別人寫好的Http服務
@Inject WSClient ws;
public Result gameMasterData(){
WSResponse response = null;
int responseStatus = 0;
String body = "";
try {
// 設定要請求的資訊
WSRequest request = ws.url("http://127.0.0.1:9000/api/getGameMasterData");
// 設定10秒後,沒友回覆的話,斷開連線
request.setRequestTimeout(10000);
// 呼叫對方寫好的Api
CompletionStage<WSResponse> responsePromise = request.get();
if(responsePromise != null){
// 取得呼叫完的結果
CompletableFuture<WSResponse> future = responsePromise.toCompletableFuture();
response = future.get();
responseStatus = response.getStatus(); // Http回覆碼
body = response.getBody(); // 回覆的資訊
} else {
responseStatus = 503;
body = "呼叫逾時";
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return ok("responseStatus = " + responseStatus + " , body = " + body);
}
}
conf/routes
# http://127.0.0.1:9000/call/gameMasterData
GET /call/gameMasterData controllers.CallController.gameMasterData()
Request
連線正常,沒有逾時。這樣我們就可以取得GM的資料了。
GET : http://127.0.0.1:9000/call/gameMasterData
Response
responseStatus = 200 , body = {"name":"GM","sex":"Male","race":"human"}
Request
連線正常,但是超過逾時。
GET : http://127.0.0.1:9000/call/gameMasterData
Response
responseStatus = 503 , body = "呼叫逾時"
Call - GET with queryString
我們呼叫Api查詢Pokemon的資料。 app/controller/CallController.java
public Result findPokemon(String pokemonName){
WSResponse response = null;
int responseStatus = 0;
String body = "";
try {
// 設定要請求的資訊,如果要帶參數,可以使用QueryParameter方式給值
WSRequest request = ws.url("http://127.0.0.1:9000/api/findPokemon")
.setQueryParameter("pokemonName", pokemonName);
// 設定10秒後,沒友回覆的話,斷開連線
request.setRequestTimeout(10000);
// 呼叫對方寫好的Api
CompletionStage<WSResponse> responsePromise = request.get();
if(responsePromise != null){
// 取得呼叫完的結果
CompletableFuture<WSResponse> future = responsePromise.toCompletableFuture();
response = future.get();
responseStatus = response.getStatus(); // Http回覆碼
body = response.getBody(); // 回覆的資訊
} else {
responseStatus = 503;
body = "\"呼叫逾時\"";
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return ok("responseStatus = " + responseStatus + " , body = " + body);
}
conf/routes
# http://127.0.0.1:9000/call/findPokemon?pokemonName=Pikachu
GET /call/findPokemon controllers.CallController.findPokemon(pokemonName : String)
Request
連線正常,沒有逾時。這樣我們就可以取得Pikachu的資料。
GET : http://127.0.0.1:9000/call/findPokemon?pokemonName=Pikachu
Response
responseStatus = 200 , body = {"name":"皮卡丘","hp":50,"skill":"打雷","lv":"5","type":"電系"}
Request
連線正常,查不到資料時。
GET : http://127.0.0.1:9000/call/findPokemon?pokemonName=XXX
Response
responseStatus = 200 , body = "查無神奇寶貝資料"
Request
連線正常,連線逾時。
GET : http://127.0.0.1:9000/call/findPokemon?pokemonName=XXX
Response
responseStatus = 503 , body = "呼叫逾時"
Call - POST with JSON Data
這次我們嘗試在Controller組好Json格式資料,來去呼叫我們寫好的Api findPokemons。
app/controller/CallController.java
public Result findPokemons(){
WSResponse response = null;
int responseStatus = 0;
String body = "";
try {
ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
arrayNode.add("Pikachu");
arrayNode.add("Bulbasaur");
arrayNode.add("XXXX");
arrayNode.add("Charmander");
arrayNode.add("Squirtle");
// 組出 json 格式
String jsonStr = "{\"pokemonNames\" : " + arrayNode.toString() + "}";
JsonNode json = Json.parse(jsonStr);
// 設定要請求的資訊,設定我們要使用json格式跟對方請求資料
WSRequest request = ws.url("http://127.0.0.1:9000/api/findPokemons")
.setHeader("Content-Type", "application/json");
// 設定10秒後,沒友回覆的話,斷開連線
request.setRequestTimeout(10000);
play.Logger.info("data = " + Json.toJson(json));
// 呼叫對方寫好的Api
CompletionStage<WSResponse> responsePromise = request.post(json);
if(responsePromise != null){
// 取得呼叫完的結果
CompletableFuture<WSResponse> future = responsePromise.toCompletableFuture();
response = future.get();
responseStatus = response.getStatus(); // Http回覆碼
body = response.getBody(); // 回覆的資訊
} else {
responseStatus = 503;
body = "\"呼叫逾時\"";
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return ok("responseStatus = " + responseStatus + " , body = " + body);
}
conf/routes
# http://127.0.0.1:9000/call/findPokemons
GET /call/findPokemons controllers.CallController.findPokemons()
Request
連線正常,沒有逾時。取得所有相關的神奇寶貝資料。
GET : http://127.0.0.1:9000/call/findPokemons
Response
responseStatus = 200 , body = [{"name":"皮卡丘","hp":50,"skill":"打雷","lv":"5","type":"電系"},{"name":"傑尼龜","hp":60,"skill":"水炮","lv":"5","type":"水系"},"查無神奇寶貝資料",{"name":"小火龍","hp":40,"skill":"噴射火焰","lv":"5","type":"火系"},{"name":"妙花種子","hp":55,"skill":"飛葉快刀","lv":"5","type":"草系"}]
Request
連線正常,連線逾時。
GET : http://127.0.0.1:9000/call/findPokemon?pokemonName=XXX
Response
responseStatus = 503 , body = "呼叫逾時"
從這個範例看到,我們成功使用POST JSON Data去Call我們寫好的Api。要注意的是我們的JSON Value後半部因為是屬於JsonArray,要注意JSON格式不要錯誤了,不然可能會呼叫錯誤。
[Final]
這個章節介紹怎樣去寫RestFul Api跟Call別人的Http RestFul Api。其中還有一項並沒有介紹,使用Html常見的Form(表單)格式來去Call別人的Api,這部份可以參考Reference的JavaWS來實作練習看看。
我們可以觀察到,當我們使用Play WS去呼叫別人的Api時,有一段呼叫的程式碼都很相似,建議可以抽出成共用類別,減少重覆的程式碼。另ㄧ方面在Call別人的Api的網址時,都需要寫URL字串來去設定網址,這部份可以把需要呼叫的Api網址,集中成一個類別管理,方便我們集中管理,減少之後如果需要更換網址時,不會因為散落各地而無從整理。
下一個章節,將會開始練習Play跟DateBase(資料庫)怎樣去作串接跟使用,靜請期待吧!!
[Reference]
1.jackson-how-to-transform-jsonnode-to-arraynode-without-casting
http://stackoverflow.com/questions/16788213/jackson-how-to-transform-jsonnode-to-arraynode-without-casting
2.JSON Tutorial
http://www.w3schools.com/json/
3.簡明RESTful API設計要點
https://tw.twincl.com/programming/*641y
4.JavaWS
https://www.playframework.com/documentation/2.5.x/JavaWS
5.java - How to extract results from wsresponse playws java
http://stackcode.xyz/sc?id=is38387780