Unit 11 - RESTful Service 使用 JAX-RS 2.0 (2): 建立應用程式

8 minute read

JSF 教學

Unit 11

事前準備

  1. 安裝 cygwin,我們需要使用 curl 指令進行測試。
  2. 使用 Netbeans 建立 Java Web 專案。

也可以先複習 RESTful Service 基本觀念介紹

User Story

JAX-RS 應用程式的 URI 規劃如下:

URI Request Method and Content Type Response and content type
/dishes GET (text/plain) 餐點名稱的清單
/dishes POST (text/plain) 新增餐點到清單。回傳新增餐點的 URL
/dishes/{id} PUT (text/plain) 更新現有的餐點名稱
/dishes/{id} DELETE (text/plain) 刪除現有的餐點名稱

步驟概覽

  1. 建立一個餐點儲存庫,並註記為 Singleton EJB,讓容器管理器生命。
  2. 繼承 javax.ws.rs.core.Application 建立一個類別,用來設定:1)RESTful 應用程式的路徑及要包含程式中的那些資源類別(Resource Class)。
  3. 建立一個資源類別,表示一個 RESTful server 上的資源。
  4. 實作資源類別中的類別方法,處理不同的 HTTP methods:
    • GET
    • POST
    • PUT
    • DELETE

實作步驟

儲存庫建立

Step 建立一個餐點庫 DishRespoBean bean,可對餐點名稱進行 CRUD 維護。 使用 Singleton EJB 進行實作, 在類別名稱宣告前使用 @Singleton 註記。 Application Server 產生此 EJB 後,會執行註記 @PostConstructinit() 方法,在餐點庫中加入兩個餐點名稱。

此餐點庫會自動維護餐點名稱的編號。

此餐點庫提供下列的方法以進行 CRUD 的操作:

  • create(String dishName): int: 建立餐點名稱,完成後回傳餐點的編號。
  • query(Integer id):String: 使用餐點編號查詢餐點名稱。
  • queryAll():List<String>: 回傳所有的餐點名稱。
  • update(Integer id, String newDishName): boolean: 更新餐點名稱, 回傳是否操作成功。
  • delete(Integer id): boolean:刪除餐點名稱, 回傳是否操作成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package repository;
  
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
  
/**
 * 餐點名稱儲存庫
 * @author hychen39@gm.cyut.edu.tw
 */
@Singleton
public class DishRespoBean {
  
    private Map<Integer, String> repository;
    private Integer lastIndex;
  
    public DishRespoBean() {
        repository = new HashMap<>();
        lastIndex = 1;
    }
  
    @PostConstruct
    public void init(){
        this.create("香酥馬芝拉條");
        this.create("雙起司辣香雞翅佐烤餅");
    }
  
    public int create(String dishName){
        int currentIdx = lastIndex;
        repository.put(currentIdx, dishName);
        lastIndex++;
        return currentIdx;
    }
  
    public boolean update(Integer id, String newDishName){
        if (repository.containsKey(id)){
            repository.replace(id, newDishName);
            return true;
        } else 
            return false;
    }
  
    public boolean delete(Integer id){
        if (repository.containsKey(id)){
            repository.remove(id);
            return true;
        } else 
            return false;
    }
  
    public String query(Integer id){
        if (repository.containsKey(id)){
            return repository.get(id);
        } else
            return null;
    }
  
    public List<String> queryAll(){
        List<String> results = new ArrayList<>();
        repository.forEach((k, v)->{
            results.add(k.toString() + ":" + v + ";");
        });
  
        return results;
    }
}
  

RESTful 應用程式的設定及

Step 繼承 javax.ws.rs.core.Application 類別,建立 AppConfig 類別,用來設定 RESTful application. 此類別可以設定:

  • RESTful 應用程式的路徑,使用 @ApplicationPath 註記
  • 指定要包含哪些的 Resource Class,預設是包含所有的 Resource Class。

此類別被註記為 @ApplicationPath("rest"),RESTful 服務的完整 URL 為:

1
http://hostname/web_context/rest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
  
package rest_resources;
  
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
  
/**
 * REST App Configuration class 
 * @author hychen39@gm.cyut.edu.tw
 * @since Mar 1, 2019
 */
@ApplicationPath("rest")
public class AppConfig extends Application{
  // The default behavior is to include all the resource classes.
}
  

資源類別的建立

Step 建立 Resource class DishesResources.

此類別為 Stateless EJB,在其名稱前註記 @Stateless

此類別對應的 Resource root uri 為 /dishes,使用註記 @Path("/dishes") 表示。此資源的 URL 為:

1
http://hostname/web_context/rest/dishes

此外,此類別需要餐點儲存庫,所以我們建立 dishStorage 的欄位,並由 App Server 注入(inject) DishRespoBean EJB 讓我們使用。

1
2
3
4
5
6
7
@Stateless
@Path("/dishes")
public class DishesResource {
    @EJB
    private DishRespoBean dishStorage;
...
}

處理 GET request

Step 建立 Resource Class method getNames() ,處理對於 /rest/dishes uri 的 GET 請求。

使用 @GET 註記此方法對應的 HTTP 方法,@Produces 註記設定回傳的內容格式為一般文字。getNames() 回傳的物件型態為 Response 物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response getNames(){
     // 取得所有的菜名
     List<String> allDishes = dishStorage.queryAll();
     // List<String> 轉 String
     String results = allDishes.toString();
     // 回傳 Response 物件
     return Response
             .ok()
             .entity(allDishes.toString())
             .build();
}

ResponseResponseBuilder 物件的關係

使用 ResponseBuilder 物件建立 Response 物件,此 API 的設計採用 「建造者」(builder)設計樣式ResponseBuilder 物件提供方法設定 HTTP Response 的 Header 及 Entity,之後將其組裝建造出一個 Response 物件。

Response 類別提供不同的靜態方法產生 ResponseBuilder 物件。ok() 建立回覆狀態 OK 的 ResponseBuilder 物件。entity() 設定 HTTP Response 的 payload 的值。當完成設定後,呼叫 build() 實際建立一個 Response 物件。

Step 測試是否能取得所有的餐點名稱。

1
curl  http://localhost:8080/unit11/rest/dishes

處理 POST request

Step 建立 Resource Class method addDish(),處理對於 /rest/dishes uri 的 POST 請求。

為了讓 addDish() 能對於 /rest/dishes uri 下的 HTTP POST 操作,註記 @POST@Consumes(MediaType.TEXT_PLAIN) 設定此方法接受的內容格式為一般文字。

addDish() 有兩個參數:

  • @Context UriInfo ui:JAX-RS 會注入 Request 的 URI
  • InputStream requestBody:JAX-RS 會注入 Request body 的串流,供讀取 Request body 的內容。JAX-RS 讀取中文時會產生亂碼,所以自行讀取 Request body 內的內容。
1
2
3
4
5
6
7
8
@POST
@Consumes(MediaType.TEXT_PLAIN)
public Response addDish(
    @Context UriInfo ui
    // Inject the InputStream to read the Chinese Characters
    , InputStream requestBody)
    ...
}

完整的 addDish() 的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@POST
@Consumes(MediaType.TEXT_PLAIN)
public Response addDish(@Context UriInfo ui
        // Inject the InputStream to read the Chinese Characters
        , InputStream requestBody) throws IOException{
  
    // Read the request body
    BufferedReader bufferReader = new BufferedReader(new InputStreamReader(requestBody));
    // 如果寫入儲存庫的資料為亂碼, 在建立 Reader 時加入 encoding 參數
    // 參考: https://docs.oracle.com/javase/7/docs/api/java/io/InputStreamReader.html#InputStreamReader(java.io.InputStream,%20java.lang.String)
    // BufferedReader bufferReader = new BufferedReader(new InputStreamReader(requestBody, "UTF-8"));
    String inputLine;
    StringBuilder sb = new StringBuilder();
    while ( (inputLine = bufferReader.readLine()) != null){
        sb.append(inputLine);
    }
  
    // Add the dish to the dish storage
    int id = dishStorage.create(sb.toString());
  
    // Print out to server log
    System.out.println(ui.getAbsolutePath().toString());
    System.out.println("Content:" + sb.toString());
  
    // Create the new url for the new dish
    // The new url pattern: http://localhost:8080/unit11/rest/dishes/{newDishID}
    URI newURI = ui.getAbsolutePathBuilder().path(String.valueOf(id)).build();
  
    // Create the response use ResponseBuilder object
    // Create a ResponseBuilder object with the entity containing the new URI.
    ResponseBuilder rb = Response.created(newURI);
    return rb.status(Response.Status.OK)
            .entity("Create a new dish")
            .type(MediaType.TEXT_PLAIN_TYPE.withCharset("utf-8"))
            .build();
}

Step 測試 POST 操作

測試指令

1
$curl -i  -X POST --header "accept: text/plain; charset=utf-8" --header "content-type: text/plain; charset=utf-8"  --data "新菜色1"  http://localhost:8080/unit11/rest/dishes

測試結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    24  100    17  100     7     17      7  0:00:01 --:--:--  0:00:01   774
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Location: http://localhost:8080/jsf_under_unit11/rest/dishes/4
Content-Type: text/plain; charset=utf-8
Date: Fri, 01 Mar 2019 05:53:28 GMT
Content-Length: 17
  
Create a new dish
  
$ curl -i http://localhost:8080/jsf_under_unit11/rest/dishes    
  
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    87  100    87    0     0     87      0  0:00:01 --:--:--  0:00:01  2806
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Content-Type: text/plain; charset=utf-8
Date: Fri, 01 Mar 2019 05:53:31 GMT
Content-Length: 87
  
[1:香酥馬芝拉條;, 2:雙起司辣香雞翅佐烤餅;, 3:新菜色;, 4:新菜色1;]

在 curl 結果中顯示 response heander 參考 [3] 在 windows command console 下要顯示中文,參考 [2]。

處理 PUT request,更新現有的菜名

PUT request 的 URI 為: /dishes/{dishID}{dishID} 的內容替換成餐點的編號,新的餐點的名稱放在 Request Payload 中。例如 http://hostname:8080/jsf_under_unit11/rest/dishes/1 的 URL 更新編號 1號餐點的名稱。

Step 建立 updateDish(InputStream, @PathParam("dishID") String, ): Response 類別方法,處理 PUT request。

1
2
3
4
5
6
7
@PUT
@Path("/{dishID}")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public Response updateDish(InputStream requestBodyStream, 
       @PathParam("dishID") String dishIDStr) throws IOException{
       }

其中

  • @PUT 註記此方法處理 PUT request。
  • @Path(/subpath) 為此方法的 URI, 完整的 URI,搭配此類別的 URI 後,為 /dishes/{dishID}
  • @Produces@Consumes 註記標示此方法可接受及產生的內容格式。

updateDish() 有兩個參數,第一個 requestBodyStream:InputStream 用來讀取 HTTP Request Payload 的內容,dishIDStr:String 則是由 JAX-RS 注入的內容,其值為 URI 中的路徑參數 {dishID}

此方法的回傳型態為 Response 類別物件。

完整的 updateDish() 的內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PUT
@Path("/{dishID}")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public Response updateDish(InputStream requestBodyStream, 
       @PathParam("dishID") String dishIDStr) throws IOException{
    int dishID = Integer.parseInt(dishIDStr);
    // Read the request body
    String dishName = MessageBodyUtil.readBody(requestBodyStream);
  
    boolean flag = dishStorage.update(dishID, dishName);
  
    // make the response message
    String msg;
    if (flag){
        msg = "Update Success";
    } else
        msg = "Update fail";
  
    return Response.ok(msg).build();
}

讀取 Request Body 的工作交由 MessageBodyUtil.readBody(InputStream):String 執行,此方法的內容如下:

1
2
3
4
5
6
7
8
9
10
11
 static public String readBody(InputStream inputStream) throws IOException{
        StringBuilder sb = new StringBuilder();
        try(BufferedReader bf = new BufferedReader(new InputStreamReader(inputStream))){
            Stream<String> stream = bf.lines();
            stream.forEach((s)->{
                sb.append(s);
            });
        }
  
        return sb.toString();
    }

此方法開啟 InputStream, 之後讀取其內容。

Step 測試 POST 操作

1
$curl -i  -X PUT --header "accept: text/plain; charset=utf-8" --header "content-type: text/plain; charset=utf-8"  --data "新菜色1"  http://localhost:8080/jsf_under_unit11/rest/dishes/2

測試結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -i  -X PUT --header "accept: text/plain; charset=utf-8" --header "content-type: text/plain; charset=utf-8"  --data "新菜色1"  http://localhost:8080/jsf_under_unit11/rest/dishes/2
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    21  100    14  100     7     14      7  0:00:01 --:--:--  0:00:01   446
HTTP/1.1 200 OK
Server: GlassFish Server Open Source Edition  4.1
X-Powered-By: Servlet/3.1 JSP/2.3 (GlassFish Server Open Source Edition  4.1  Java/Oracle Corporation/1.8)
Content-Type: text/plain; charset=utf-8
Date: Tue, 05 Mar 2019 07:35:02 GMT
Content-Length: 14
  
Update Success
  
user@DESKTOP-HQO01TF ~
$ curl  http://localhost:8080/jsf_under_unit11/rest/dishes                        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    38  100    38    0     0     38      0  0:00:01 --:--:--  0:00:01  1225
[1:香酥馬芝拉條;, 2:新菜色1;]
user@DESKTOP-HQO01TF ~
$

處理 DELETE request,刪除現有的菜名

刪除餐點的 URI 為 /dishes/{dishID}, 使用 HTTP DELETE 方法進行操作。

Step 建立 removeDish() 物件方法並加註相關註記如下:

1
2
3
4
5
@Path("/{dishID}")
@DELETE
public Response removeDish(@PathParam("dishID") String dishIDStr){
  ...
}

其中

  • @Path(/subpath) 為此方法的 URI, 完整的 URI,搭配此類別的 URI 後,為 /dishes/{dishID}
  • @DELETE 註記此方法對應的 HTTP 方法

此方法的參數 dishIDStr 為 JAX-RS API 注入的 URI 的路徑參數 {dishID} 的值。

完整的 removeDish() 的內容:

1
2
3
4
5
6
7
8
9
@Path("/{dishID}")
    @DELETE
    public Response removeDish(@PathParam("dishID") String dishIDStr){
        Integer dishID = Integer.parseInt(dishIDStr);
        boolean flag = dishStorage.delete(dishID);
  
        String msg = flag?"Delete Success":"Delete Fail";
        return Response.ok(msg).build();
}

Step 測試 DELETE 操作 測試指令:

1
curl -X DELETE http://localhost:8080/jsf_under_unit11/rest/dishes/2

測試結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
user@DESKTOP-HQO01TF ~
$ curl  http://localhost:8080/jsf_under_unit11/rest/dishes
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    23  100    23    0     0     23      0  0:00:01 --:--:--  0:00:01   500
[1:香酥馬芝拉條;]
user@DESKTOP-HQO01TF ~
$ curl -X DELETE http://localhost:8080/jsf_under_unit11/rest/dishes/1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    14  100    14    0     0     14      0  0:00:01 --:--:--  0:00:01   297
Delete Success
user@DESKTOP-HQO01TF ~
$ curl  http://localhost:8080/jsf_under_unit11/rest/dishes
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100     2  100     2    0     0      2      0  0:00:01 --:--:--  0:00:01    42
[]
user@DESKTOP-HQO01TF ~
$

References

  1. Using Curl in Java, https://www.baeldung.com/java-curl
  2. 在命令提示視窗(Command Prompt)顯示UTF-8內容-黑暗執行緒
  3. View header and body with curl – Rob Allen's DevNotes

Updated: