差異處

這裏顯示兩個版本的差異處。

連向這個比對檢視

java:web:restapi:http_method_cant_make_sense [2020/12/08 00:33]
tony
java:web:restapi:http_method_cant_make_sense [2023/06/25 09:48]
行 1: 行 1:
-{{tag>​rest}} 
-====== Http Method無法表達出某些動作 ====== 
-===== Introduction ===== 
-在設計RestAPI時,並非所有domain操作都符合CRUD;例如,model中資料的同步(sync)、有transaction的行如轉換(transfer)、搬移(move)、複製(copy)等;因此做些案例研究看是否能找尋到自己能滿意的做法。本篇文章是根據研究結果,分享針對操作(action)做resource modeling的心得。 
-===== Model the action as a service resource ===== 
-action本身就是一種service,如果能把它model成一個service resource,是再好不過的。最常見的例子是login/​logout,可以model為sessions,以[[https://​pubs.vmware.com/​vcd-55/​index.jsp|vCloud]]為例:​ 
-<​code>​ 
-POST https://​vcloud.example.com/​api/​sessions ​ 
-DELETE https://​vcloud.example.com/​api/​sessions 
-</​code>​ 
-在伺服器管理的domain中,可能有像修改firmware設定、更新firmware或掃描硬體等操作,這些該如何model為resource呢?​ 首先是修改firmware設定,以[[http://​h20566.www2.hpe.com/​hpsc/​doc/​public/​display?​docId=c04423967|HPE Server Management API]]為例,它將firmware種類model為一種resource,而設定是它的sub-resource:​ 
-<​code>​ 
-GET /​Systems/​1/​BIOS/​Settings 
-PATCH /​Systems/​1/​BIOS/​Settings 
-</​code>​ 
-接著是更新firmware,以[[https://​cpsdocs.dellemc.com/​bundle/​PFMGR_API_3/​page/​GUID-87FE88BA-FE97-4ECA-BF1E-46A23CC6414F.html|Dell ASM REST API]]為例,使用store resource的方式去操作:​ 
-<​code>​ 
-PUT /​ManagedDevice/​firmware 
-</​code>​ 
-最後是掃描硬體,以[[https://​access.redhat.com/​documentation/​en-US/​Red_Hat_Enterprise_Virtualization/​3.0/​html-single/​REST_API_Guide/​index.html|RHEL Virtualization]]為例,透過post去新增一個discover的sub-resource(task):​ 
-<​code>​ 
-POST /​api/​hosts/​2ab5e1da-b726-4274-bbf7-0a42b16a0fc3/​iscsidiscover HTTP/1.1 
-Accept: application/​xml 
-Content-Type:​ application/​xml 
  
-<​action>​ 
-    <​iscsi>​ 
-        <​address>​mysan.example.com</​address>​ 
-    </​iscsi>​ 
-</​action>​ 
- 
-HTTP/1.1 202 Accept 
-Content-Type:​ application/​xml 
- 
-<action id="​e9126d04-0f74-4e1a-9139-13f11fcbb4ab"​ 
-  href="/​api/​hosts/​2ab5e1da-b726-4274-bbf7-0a42b16a0fc3/​iscsidiscover/​ 
-  e9126d04-0f74-4e1a-9139-13f11fcbb4ab">​ 
-    <​iscsi_target>​iqn.2009-08.com.example:​mysan.foobar</​iscsi_target>​ 
-    ... 
-<​action>​ 
-</​code>​ 
-看起來好像很容易,但要做到這些需要經驗與想像力,否則model出來的resource可能會讓user覺得困惑。除此之外,還要考量是否有非同步需求;以更新firmware來說,雖然PUT不是不能回傳202,但比較少看到有人使用。由於以上原因,接下來我要分享比較簡單且有不少案例的方法。 
-===== A simple way: store resource/​PATCH/​controller resource ===== 
-這個方法是根據[[http://​www.vinaysahni.com/​best-practices-for-a-pragmatic-restful-api|這篇best practice]]、 
-[[http://​shop.oreilly.com/​product/​0636920021575.do|REST API Design Rulebook]]與case studies整理出來的,給大家當一個參考方法。\\ 
-我們以下面的動作為例:​ 
-<​code>​ 
-lock file 
-unlock file 
-power on 
-power off system 
-activate user 
-deactivate user 
-login  
-logout 
-search 
-import 
-update firmware 
-config bios 
-</​code>​ 
-==== Classify the actions ==== 
-首先我們必須先分類;目前我分成兩類,\\ 
-1. The positive and negative action,動作有正向與反向:​ 
-<​code>​ 
-lock/unlock file 
-power on/off system 
-activate/​deactivate 
-login/​logout 
-</​code>​ 
-2. The procedural action,動作是一個執行過程:​ 
-<​code>​ 
-search 
-import 
-update firmware 
-config bios 
-</​code>​ 
-==== The positive and negative action ==== 
-假如你的動作是屬於這個種類,你有兩個選擇, 
-  * Treat it like a sub-resource. 
-  * Patch for the partial update. 
-如果可以是resource中的屬性,可以考慮用PATCH的方式去更新屬性值:​ 
-<​code>​ 
-Patch /​Users/​12345 
-{“active”:​ false} 
-</​code>​ 
-另外一個選擇就是把它當sub-resource,也就是store resource的做法;positive的action使用PUT,negative的action使用DELETE。以[[https://​developer.github.com/​v3/​issues/​|github]]與[[https://​developer.box.com/​reference|box]]為例:​ 
-<​code>​ 
-lock/unlock (github) 
-    PUT /​repos/:​owner/:​repo/​issues/:​number/​lock 
-    DELETE /​repos/:​owner/:​repo/​issues/:​number/​lock 
-apply/​remove the watermark (box) 
-    PUT https://​api.box.com/​2.0/​files/​file_id/​watermark 
-    DELETE https://​api.box.com/​2.0/​files/​file_id/​watermark 
-</​code>​ 
- 
-==== The procedural action ==== 
-這個能列舉的範例很多,只要不是CRUD的操作,你可以選擇把它model為controller resource: 
-<​code>​ 
-Search 
-    POST /​v1/​users/​search (instagram) 
-    POST /​1/​indexes/​{indexName}/​query (algolia) 
-    POST /​indexes/​hotels/​docs/​search (azure) 
-    POST facebook, twitter, box, github, etc.. 
-POST /​videos/​reportAbuse (youtube) 
-</​code>​ 
-如果不想濫用,你可以把動作是非同步做為前置條件。 
-===== Case Study: Power Management ===== 
-以上述的方法,我們來討論電源管理該怎麼做。首先我們把電源當成一個resource,而狀態為其屬性;所以我們可以透過GET去取得狀態,而透過PUT或PATCH去修改狀態:​ 
-<​code>​ 
-GET /​hosts/​123/​power 
-{'​state':'​on'​} 
-PUT /​hosts/​123/​power 
-{'​state':'​on'​} 
-PUT /​hosts/​123/​power 
-{'​state':'​off'​} 
-</​code>​ 
-這樣的做法,有哪些問題我們需要考量?​ 
-  - hypermedia: 我們有辦法表達出取得狀態、關機、開機等的link嗎?​ 
-  - synchronized:​ 在我們做開機與關機動作後,是可以立即反應的嗎? ​ 
-  - implementation:​ 如果使用PATCH的話,我比較不是那麼喜愛,原因稍後再做說明。 
-考慮以上原因,或許把它做成store resource會比較好:​ 
-<​code>​ 
-GET /​hosts/​123/​power_status 
-{'​state':'​on'​} 
-PUT /​hosts/​123/​power_on 
-PUT /​hosts/​123/​power_off 
-</​code>​ 
-這裡我並沒有使用DELETE,因為在語義上使用DELETE power_on不會比PUT power_off來得清楚。此外,在使用這種方式後,hypermedia可以很容易的使用URI去表達出不同的意義。剩下的問題就是synchronized。\\ 
-\\ 
-PUT回傳202的方式,目前有看過[[https://​techlibrary.hpe.com/​docs/​enterprise/​servers/​oneview5.0/​cicf-api/​en/​index.html#​rest/​enclosures|HPE OneView REST API]]存在這樣的設計;HTTP規格書也沒說這樣是不對的。我覺得值得討論的部分是:​ idempotent。假設PUT回傳202,這代表著server將產生一個asynchronized的task,每次PUT power_on所產生的task是否會相同呢?​ 假如不同,是不是代表違反了idempotent?​ 假如相同,實作會不會蠻奇怪的呢?​ 這個我目前沒有答案。\\ 
-\\ 
-另外一個選擇,就是把它當controller resource,使用POST去操作:​ 
-<​code>​ 
-POST /​hosts/​123/​power_on 
-POST /​hosts/​123/​power_off 
-</​code>​ 
-有個實際案例就是[[https://​pubs.vmware.com/​vcd-51/​index.jsp?​topic=/​com.vmware.vcloud.api.reference.doc_51/​doc/​operations/​POST-PowerOffVApp.html|vCloud]]的Power On/Off API。順便提一下,會使用這個範例是由於在Roy Fielding在2008年[[http://​roy.gbiv.com/​untangled/​2009/​it-is-okay-to-use-post|It is okay to use POST]]文章中,留言區有人提及此範例;而Roy Fielding也是傾向於將狀態與操作model為不同狀態,這使我思考為什麼他會這樣想。 
- 
-===== Some questions ===== 
-針對以上提到的方法,我還思考著幾個問題:​ 
-==== Why don't you use the query string or formdata to pass the action? ==== 
-(這裡先撇開query string、formdata或request body等方式的差異) 對我而言,我目前認為有幾點需要考量:​ 
-  - hypermedia: 假如要表達出query string等方式,會需要使用template的方式,實作上不會比單純透過URI Path的方式容易。 
-  - http method convention: 如果在collection上使用POST,是否會讓新增與其它action讓使用者混淆。 
-  - implementation:​ 我們看看如果使用URI的Path來實做lock與unlock可能會長怎樣:​ 
-<code java> 
- @RequestMapping(value = "/​files/​{fid}/​lock",​ method = RequestMethod.PUT,​ produces = {"​application/​json"​}) 
- public OutputData<​HostRestBean>​ lockFile(@PathVariable("​fid"​) String aFid, HttpServletResponse ​ aResponse){ 
- // ... 
- } 
- 
- @RequestMapping(value = "/​files/​{fid}/​unlock",​ method = RequestMethod.DELETE,​ produces = {"​application/​json"​}) 
- public OutputData<​HostRestBean>​ unlockFile(@PathVariable("​fid"​) String aFid, HttpServletResponse ​ aResponse){ 
- // ... 
- } 
-</​code>​ 
-接著使用POST+RequestParam:​ 
-<code java> 
- @RequestMapping(value = "/​files/​{fid}",​ method = RequestMethod.POST,​ produces = {"​application/​json"​}) 
- public OutputData<​HostRestBean>​ opFile(@PathVariable("​fid"​) String aFid,  
- @RequestParam(value="​action",​ required=true) String action, 
- HttpServletResponse ​ aResponse) { 
- if("​lock"​.equalsIgnoreCase(action)) { 
- // ... 
- } else if("​unlock"​.equalsIgnoreCase(action)){ 
- // ... 
- } else 
- // ... 
- } 
-</​code>​ 
-你喜歡哪個?​ 假如File的action只有lock與unlock,那真的是天下太平;但事實上,action還有move、copy、rename等。考慮一下測試、維護、擴充的話,哪一個會比較好?​ 以擴充與維護來說,RequestParam的方式讓所有的action都必須接受同一組參數甚至輸出,增加了修改的麻煩;URI Path則由各別的實做去決定。測試則是因為實做已分開,根據各別操作的目的去測試即可。 
-==== Threat the P/N action as a store resource or use PATCH? ==== 
-在我們的專案中,P/​N action我傾向使用store resource大於PATCH。主要原因有以下:​ 
-  * 合適的media type: PATCH所能使用的media type種類相當多,對於同時支援xml與json格式的我們,要選哪一條路還無定論。PATCH種類有機會再介紹。 
-  * 不容易寫好維護的程式:​ 由於PATCH是部分更新,所以程式必須針對開放欄位做是否修改的判斷,也可能要為此打造輸入使用的物件。 
-<code java> 
-@PatchMapping(value = "/​files/​{id}"​) 
-public ResponseEntity<​String>​ editFile(@RequestBody FileBean updateFile, ​ 
- @PathVariable("​id"​) String fileId){ 
- File file = FileDao.getFile(fileId);​ 
- if( file == null ) { 
- return new ResponseEntity<​String>​("​File doesn'​t exist", ​ HttpStatus.NOT_FOUND);​ 
- } 
-  
- if( updateFile.locked != null ) 
- file.locked = Boolean.parseBoolean(updateFile.locked);​ 
- if( updateFile.name != null ) 
- file.name = updateFile.name;​ 
- return new ResponseEntity<​String>​("​good", ​ HttpStatus.OK);​ 
-} 
-</​code>​ 
-  * 符合多種情況的request參數:​ 面對不同的傳遞參數方式,目前我還沒找到一個共用的方法。 
-這樣抉擇的發生,是建立在動作屬於Resource中的一個attribute時。 
-===== Summary ===== 
-面對一個非CRUD的domain操作,我們該如何model為resource呢? ​ 
-  - 盡力把它變成service resource,可參考他人做法。 
-  - 確認是否為一個procedure,是的話就把它當controller resource;可以把非同步的性質當前置條件。 
-  - 確認是否為resource中的attribute,是的話可以使用PATCH。 
-  - 如果不想用PATCH,可以考慮作為store resource。 
-以上方法可以當參考,還是要以需求為重;另外補充我看過的[[.:​http_method_cant_make_sense:​other_case_studies|其它的案例]]。 
-===== Reference ===== 
-  * [[https://​stackoverflow.com/​questions/​2173721/​why-does-including-an-action-verb-in-the-uri-in-a-rest-implementation-violate-th|Why does including an action verb in the URI in a REST implementation violate the protocol?]] 
-  * [[http://​shop.oreilly.com/​product/​0636920021575.do|REST API Design RuleBook]] Rule: A verb or verb phrase should be used for controller names 
-  * [[https://​stackoverflow.com/​questions/​27121749/​confusion-between-noun-vs-verb-in-rest-urls|Confusion Between Noun vs. Verb in Rest URLs]] 
-  * [[https://​apihandyman.io/​do-you-really-know-why-you-prefer-rest-over-rpc/​|Do you really know why you prefer REST over RPC?]] 
-  * [[https://​developer.mozilla.org/​en-US/​docs/​Web/​HTTP/​Methods/​OPTIONS|HTTP OPTIONS method]] 
-  * [[https://​www.smashingmagazine.com/​2016/​09/​understanding-rest-and-rpc-for-http-apis/​|Understanding rest and rpc for http apis]] 
-  * [[https://​stackoverflow.com/​questions/​24241893/​rest-api-patch-or-put|Patch VS PUT Store Resource]] 
-  * [[https://​www.instagram.com/​developer/​endpoints/​|Instangram REST API]] 
-  * [[http://​roy.gbiv.com/​untangled/​2009/​it-is-okay-to-use-post|it is okay to use post]] 
-  * [[http://​www.vinaysahni.com/​best-practices-for-a-pragmatic-restful-api|Best Practices for Designing a Pragmatic RESTful API]] 
- 
-=====    ===== 
----- 
-\\ 
-~~DISQUS~~