Unit 11 - RESTful Service 使用 JAX-RS 2.0 (2): 建立應用程式
Unit 11
事前準備
- 安裝 cygwin,我們需要使用 curl指令進行測試。
- 使用 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) | 刪除現有的餐點名稱 | 
步驟概覽
- 建立一個餐點儲存庫,並註記為 SingletonEJB,讓容器管理器生命。
- 繼承 javax.ws.rs.core.Application建立一個類別,用來設定:1)RESTful 應用程式的路徑及要包含程式中的那些資源類別(Resource Class)。
- 建立一個資源類別,表示一個 RESTful server 上的資源。
- 實作資源類別中的類別方法,處理不同的 HTTP methods:
    - GET
- POST
- PUT
- DELETE
 
實作步驟
儲存庫建立
Step 建立一個餐點庫 DishRespoBean bean,可對餐點名稱進行 CRUD 維護。
使用 Singleton EJB 進行實作, 在類別名稱宣告前使用 @Singleton 註記。
Application Server 產生此 EJB 後,會執行註記 @PostConstruct 的 init() 方法,在餐點庫中加入兩個餐點名稱。
此餐點庫會自動維護餐點名稱的編號。
此餐點庫提供下列的方法以進行 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();
}
Response 及 ResponseBuilder 物件的關係
使用 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
- Using Curl in Java, https://www.baeldung.com/java-curl
- 在命令提示視窗(Command Prompt)顯示UTF-8內容-黑暗執行緒
- View header and body with curl – Rob Allen's DevNotes