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) | 刪除現有的餐點名稱 |
步驟概覽
- 建立一個餐點儲存庫,並註記為
Singleton
EJB,讓容器管理器生命。 - 繼承
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 的 URIInputStream 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