Ch8 : Play Api & Call

[Play Api & Call]
這章節我們會練習寫RestFul Api跟怎樣去呼叫別人寫好的Api,一般為了資料可以方便傳輸到Android或者iOS行動裝置上,通常會使用JSON格式來傳輸資料,以下示範,怎樣去實作這些功能。建議可以使用瀏覽器的Rest套件工具,來輔助您的測試。這裡我是使用到FireFox的附加元件RestClient來測試我寫好的API

如果不太懂JSONRestFul是什麼,可以參考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-TypeHeader資訊,是寫使用application/json;charset=utf-8格式輸出,而本身Playretrun 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,來達成我們需要的結果或畫面,以下會示範GETPOST怎樣從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 DataCall我們寫好的Api。要注意的是我們的JSON Value後半部因為是屬於JsonArray,要注意JSON格式不要錯誤了,不然可能會呼叫錯誤。


[Final]
這個章節介紹怎樣去寫RestFul ApiCall別人的Http RestFul Api。其中還有一項並沒有介紹,使用Html常見的Form(表單)格式來去Call別人的Api,這部份可以參考ReferenceJavaWS來實作練習看看。

我們可以觀察到,當我們使用Play WS去呼叫別人的Api時,有一段呼叫的程式碼都很相似,建議可以抽出成共用類別,減少重覆的程式碼。另ㄧ方面在Call別人的Api的網址時,都需要寫URL字串來去設定網址,這部份可以把需要呼叫的Api網址,集中成一個類別管理,方便我們集中管理,減少之後如果需要更換網址時,不會因為散落各地而無從整理。

下一個章節,將會開始練習PlayDateBase(資料庫)怎樣去作串接跟使用,靜請期待吧!!


[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

results matching ""

    No results matching ""