追根溯源
理解一個概念最好的一個方法就是知其來龍去脈,最簡單暴力的方式就是直接上結論。
知識并不僅僅是其表面上看起來那么枯燥乏味的,也可以很性感。就像《七堂極簡物理課》的作者,雖然是個物理學家,講的也是物理學世界里博大精深的理論,但是他以詩的語言,講訴了物理學家發現每個理論碰到的困難和驚喜,有血有肉,讓人沉浸其中。
今天,我們的主角是個IT界牛人中的牛人,意思就是BAT這樣的牛人,聽到這個名字,也會說,他是個牛人,自己只是個小學生。
REST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的。Fielding何許人也,他是HTTP協議(1.0版和1.1版)的主要設計者、Apache服務器軟件的作者之一、Apache基金會的第一任主席。這是個大牛人。
Fielding將他對互聯網軟件的架構原則,定名為REST,即Representational State Transfer的縮寫。翻譯是"表現層狀態轉化", 這個概念很重要,這里劃重點嘍。那么,如果一個架構符合REST原則,就稱它為RESTful架構。
REST——表現層狀態轉化
REST的名稱"表現層狀態轉化"中,省略了主語,主語就是資源(Resources)。 何謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。它可以是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實物。你可以用一個URI(統一資源定位符)指向它,每種資源對應一個特定的URI。
表現層(Representation),"資源"是一種信息實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層"(Representation)。 比如,文本內容可以用txt格式表現,也可以用HTML格式、XML格式、JSON格式表現,甚至可以采用二進制格式;圖片可以用JPG格式表現,也可以用PNG格式表現。
劃重點啦,狀態轉化(State Transfer)。互聯網通信協議HTTP協議,是一個無狀態協議,這意味著,所有的狀態都保存在服務器端。因此,如果客戶端想要操作服務器,必須通過某種手段,讓服務器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉化"。
客戶端用到的手段,只能是HTTP協議。具體來說,就是HTTP協議里面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET用來獲取資源,POST用來新建資源(也可以用于更新資源),PUT用來更新資源,DELETE用來刪除資源。
綜合上面的解釋,我們總結一下什么是RESTful架構:
(1)資源用URI表示;
(2)客戶端和服務器之間,在表現層進行資源的狀態轉換;
(3)客戶端通過四個HTTP動詞,對服務器端資源進行操作,實現"表現層狀態轉化"。
接口設計誤區
最常見的一種設計錯誤,就是URI包含動詞。因為"資源"表示一種實體,所以應該是名詞,URI不應該有動詞,動詞應該放在HTTP協議中。
舉例來說,某個URI是/posts/show/1,其中show是動詞,這個URI就設計錯了,正確的寫法應該是/posts/1,然后用GET方法表示show。
GET /POSTS/1
技術實現細節——網絡框架
- bean文件夾放置DTO,以后跟服務器端協定好DTO,將所有放置此處,可修改;
- requests文件夾放置網絡操作,推薦按模塊區分,一個模塊一個類,模塊下的網絡操作直接寫成一個方法,可修改;
- HttpBaseDownload實現下載方法,HttpBaseUpload實現上傳方法,但我們現在不case;
- 接下來我們重點關注HttpBaseReq這個類,這個類封裝了RestFul所需要的四個表示操作方式。
技術實現細節——核心類HttpBaseReq
看看HttpBaseReq類方法
現在直接幫你劃重點啦,方法取名已經很明確了,直接按操作動作選方法。幾個參數的意思在使用處再詳細講解。
挑取requestPut方法瞅兩眼
public static <E> int requestGetWithDialog(Context activity, String url, E mBean, final NetProcessCallback processCallback,
final NetResponseCallback callback) {
//1. 網絡請求實現
int taskId = requestImp(url, HttpRequest.RequestMethod.GET, mBean, processCallback, callback);
//2. 起小海快跑彈窗
if (taskId > 0) {
showDialog(activity, taskId);
}
//3. 返回網絡請求唯一標示
return taskId;
}
這個方法與requestPostWithDialog、requestDeleteWithDialog等不同操作代碼相同,不同動作的區分靠HttpRequest.RequestMethod這個枚舉,Get請求傳遞HttpRequest.RequestMethod.GET,post請求傳遞HttpRequest.RequestMethod.POST。
哦,寶藏藏在requestImp,讓我們繼續深挖下去
private static <E> int requestImp(String url, final HttpRequest.RequestMethod requestMethod, E mBean, final NetProcessCallback
processCallback, final NetResponseCallback callback) {
... ...
//1. 將入參Bean實體轉換未Json對象jsonObject
// class -> String
if (mGson == null) {
mGson = new Gson();
}
JsonObject jsonObject = null;
String k = null;
if (mBean != null) {
k = mGson.toJson(mBean);
// string -> JsonObject
if (sJsonParser == null) {
sJsonParser = new JsonParser();
}
jsonObject = sJsonParser.parse(k).getAsJsonObject();
k = jsonObject.toString();
}
//2. get和delete動作 將url和k組合,組合成http://baidu.com?name=jack形式
if ((requestMethod == HttpRequest.RequestMethod.GET || requestMethod == HttpRequest.RequestMethod.DELETE)
&& jsonObject != null && jsonObject.size() > 0) {
k = null;
// 將url和k組合
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(url);
stringBuilder.append("?");
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
stringBuilder.append(entry.getKey()).append("=").append(entry.getValue().toString()).append("&");
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
url = stringBuilder.toString();
url = url.replaceAll("\"", "");//臨時替換雙引號
}
int taskId = 0;
//3. 調用田少的RESTful網絡底層
taskId = HttpRequest.getInstance().requestWithRESTful(url, requestMethod, getHeadJson(), k, new NetworkCallback() {
... ...
});
if (taskId <= 0) {
callback.netFailure("請求下發失敗");
}
return taskId;
}
代碼邏輯有點多了,幫你做個小結吧
- 將入參Bean實體轉換未Json對象jsonObject;
- Delete和Get請求,需要將路徑url和入參k組合,組合成http://baidu.com?value=jack 形式;
- Post和Patch請求,將入參作為postData數據傳輸。
此時我的眼睛瞄準了requestWithRESTful
public int requestWithRESTful(String url, HttpRequest.RequestMethod method, String jsonHeadLines, String bodyData, NetworkCallback networkCallbacks) {
Log.v(Log.XH_NETWORK_CLIENT_TAG, " url=" + url);
Log.v(Log.XH_NETWORK_CLIENT_TAG, " method=" + method);
Log.v(Log.XH_NETWORK_CLIENT_TAG, " jsonHeadLines=" + jsonHeadLines);
Log.v(Log.XH_NETWORK_CLIENT_TAG, " bodyData=" + bodyData);
JSONObject jsonParam = new JSONObject();
try {
jsonParam.put("requestMethod", method.toString());
if(bodyData != null && !method.equals(HttpRequest.RequestMethod.DELETE) && !method.equals(HttpRequest.RequestMethod.GET)) {
jsonParam.put("postData", bodyData);
}
if(jsonHeadLines != null) {
jsonParam.put("headList", jsonHeadLines);
} else {
JSONObject e = new JSONObject();
e.put("contentType", "application/json");
jsonParam.put("headList", e.toString());
}
} catch (JSONException var8) {
var8.printStackTrace();
}
return this.request(url, jsonParam.toString(), networkCallbacks);
}
- 田少requestMethod里存放請求動作;
- headList放置自行協議的請求頭;
- postData放置非DELETE請求和非GET請求的請求數據。
協議的請求頭
往回掰代碼,找到getHeadJson方法
/**
* @return 返回Header頭
*/
private static String getHeadJson() {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("Content-Type", "application/json; charset=UTF-8");
String packageName = XHApplication.sPackageName;
String versionName = XHApplication.sVersionName;
String releaseName = Build.VERSION.RELEASE;
String model = android.os.Build.MODEL;
String suuid = XHApplication.sUUID;
String userAgent = String.format(Locale.getDefault(), "%s/%s (%s; android; %s; %s)",
packageName, versionName, model, releaseName, suuid);
jsonObject.addProperty("User-Agent", userAgent);
String session = AppConfig.get(AppConfig.KEY_SESSION);
if(!TextUtils.isEmpty(session)){
String auth = String.format(Locale.getDefault(), "Bearer %s", session);
jsonObject.addProperty("Authorization", auth);
}
return jsonObject.toString();
}
- Content-Type: 內容類型,可以有application/xml,text/xml,我們現在統一application/json;
- User-Agent傳輸設備、客戶端相關的信息:
格式 User-Agent: 應用名稱或包名/應用版本號 (設備型號; android|ios; 系統版本號; 5.0設備串號|U) > U表示未定義或未知
比如:User-Agent: com.xh.app.yuedu/1.0 (P350; android; 5.0; 設備串號)
- Authorization頭傳輸用戶認證的token:
格式 Authorization: Bearer sessionId;
如 Authorization: Bearer 4086c8d4-8ca4-42d3-a900-c9e7a5e8329d;
如何使用
一個Get請求
服務器端接口定義:
網絡請求:
/**
* des:獲取文章評論列表
* author:wqk
* date:2017/3/9 0009 14:46
* String exampel="http://192.168.5.29:8082/v1/articles/5/comments?userId=32673&page=0&size=10";
*/
public static void loadArticleCommentList(Context context, RequestCommentListBean bean, NetResponseCallback callback) {
String token = XHApplication.getXHApplication().getM();
bean.setUserId(token);
//0. 拼裝路徑
String url = String.format(Locale.getDefault(), "%sarticles/%s/comments", NetConst.ROOT_URL, bean.getArticleId());
HttpBaseReq.requestGetWithDialog(context, url, bean, new NetProcessCallback() {
@Override
public void dataProcessor(Object object) {
//1. String類型的數據用Gson轉換成實體類數據
ArticleCommentListResponse response = (new Gson()).fromJson(
object.toString(), ArticleCommentListResponse.class);
//2. 數據存儲,可存配置,存數據庫,存文件
AppConfig.put(NetConst.LOAD_ARTICLE_COMMENT_LIST_RESPONSE, response);
}
}, callback);
}
- url的拼接需要用戶usId和文章id,表示某人(UsId)獲取(Get)某篇文章(ArticleId)下的評論;
- NetProcessCallback用作解析數據,方法是子線程調用,不阻塞主線程,里面可執行解析數據,存儲數據操作;
- bean作為參數傳入requestGetWithDialog方法,bean的數據會以key=value的方式拼接到路徑的?后面。
一個Post請求
服務器接口定義:
網絡請求:
/**
* 保存評論,無數據返回,可不解析數據
*/
public static void saveComment(Context context, SaveCommentBean bean, NetResponseCallback callback) {
String usId = XHApplication.getXHApplication().getM();
bean.setUserId(NumberUtil.toInt(usId));
String url = String.format(Locale.getDefault(), "%sarticles/%s/comments", NetConst.ROOT_URL, bean.getArticleId());
HttpBaseReq.requestPostWithDialog(context, url, bean, null, callback);
}
- 無數據返回,可不解析數據,NetProcessCallback參數可傳null;
一個Delete請求
服務器接口定義:
/**
* 刪除評論, ?后無數據,bean入參可傳null, 無數據返回,可不解析數據
*/
public static void deleteComment(Context context, DeleteCommentBean bean, NetResponseCallback callback) {
String url = String.format(Locale.getDefault(), "%sarticles/%s/comments/%s"
, NetConst.ROOT_URL, bean.getArticleId(), bean.getCommentId());
HttpBaseReq.requestDeleteWithDialog(context, url, null, null, callback);
}