追根溯源
理解一个概念最好的一个方法就是知其来龙去脉,最简单暴力的方式就是直接上结论。
知识并不仅仅是其表面上看起来那么枯燥乏味的,也可以很性感。就像《七堂极简物理课》的作者,虽然是个物理学家,讲的也是物理学世界里博大精深的理论,但是他以诗的语言,讲诉了物理学家发现每个理论碰到的困难和惊喜,有血有肉,让人沉浸其中。
今天,我们的主角是个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);
}