先扯兩句
做個(gè)項(xiàng)目是真占時(shí)間啊图甜,不知不覺已經(jīng)三周沒有發(fā)新的博客了(這個(gè)接口不錯吧)碍粥,總算是有個(gè)周末,趁休息黑毅,將最近遇到的一些問題再次貼出來嚼摩,算是完善一下工程。
閑言少敘矿瘦,老規(guī)矩還是先上我的Git庫枕面,然后開始正文。
MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)
正文
“實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)”缚去,這句話的唯一兩字是否正確我們先放下不表潮秘,不過至少在這段做項(xiàng)目的時(shí)間中,對于之前自己所寫的內(nèi)容還是有了全新的認(rèn)識的易结,上一次發(fā)的內(nèi)容枕荞,主要是總結(jié)了一下漏洞柜候,希望看到的大家看到后能夠避免走上相同的彎路,而今天這篇呢躏精,看到標(biāo)題大家應(yīng)該也知道了渣刷,還是一些階段總結(jié)的內(nèi)容,不過這次下手的只是Retrofit矗烛,但想必誤入這些彎路的絕不止我一個(gè)辅柴,那么就進(jìn)入今天的內(nèi)容吧。
Retrofit 上傳JSON
具體還是從需求說起瞭吃,當(dāng)然碌嘀,還是網(wǎng)絡(luò)訪問的部分,那就是我參與的項(xiàng)目需要給后臺傳輸JSON數(shù)據(jù)虱而,之前也查到了一些方法筏餐,那就是在Header中添加設(shè)置Content-Type為application/json开泽,看了我前面的博客《一個(gè)Android工程的從零開始》階段總結(jié)與修改1-base的應(yīng)該會知道牡拇,其中最后一部分就是闡述的如何進(jìn)行Retrofit header的動態(tài)添加封裝,這里自然也就方便了許多穆律,只需要在初始化中加下屬這部分即可惠呼。
headerParams.put("Content-Type", "application/json");
這么簡單的操作還不是分分鐘結(jié)束戰(zhàn)斗啊,于是試了第一個(gè)接口峦耘,get請求剔蹋,果然沒有問題「ㄋ瑁可當(dāng)嘗試POST請求的時(shí)候泣崩,卻是給我返回來了服務(wù)器端的異常信息,我還憤憤的找服務(wù)器端算賬洛口,可當(dāng)看到我的請求數(shù)據(jù)矫付,真是恨不得找個(gè)地縫鉆進(jìn)去。
老子已經(jīng)進(jìn)行了設(shè)置第焰,為毛上傳的信息還是key-value形式的B蛴拧!挺举!
別問我為什么get可以杀赢,本來get就不管上傳什么形式的,統(tǒng)統(tǒng)url湘纵,自然不可能出問題脂崔。
為了告訴后臺我確實(shí)上傳的是JSON形式的參數(shù),還將傳遞的參數(shù)攔截了下來梧喷,給他們看:
攔截方法如下:
private void initBaseData(boolean isFromLogin) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS);
builder.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
LogUtil.info("responseString", "request====" + action);
LogUtil.info("responseString", "request====" + request.headers().toString());
LogUtil.info("responseString", "request====" + request.toString());
okhttp3.Response proceed = chain.proceed(request);
LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
return proceed;
}
});
Retrofit.Builder builder1 = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
builder1.baseUrl(baseUrl);
retrofit = builder1.build();
}
從中也可以看出我們的Retrofit只是對于OkHttp的一種封裝脱篙,具體到一些功能性的內(nèi)容娇钱,還是得從OkHttp下手,這點(diǎn)我也很無奈啊绊困。
不過需要說明的一點(diǎn)是文搂,大家可以看到,這里我們只輸出的Header對應(yīng)的信息秤朗,卻沒有對Body下手煤蹭,這個(gè)真不是我不想,而是真心無能為例取视,body下只有一個(gè)toString方法硝皂,輸出的還是對應(yīng)body的Id,所以大家如果想查看自己的傳輸?shù)腷ody的信息作谭,暫時(shí)我能提供的建議稽物,就只有通過斷點(diǎn)調(diào)試了。
從proceed.headers().toString()對應(yīng)的輸出語句中確實(shí)可以看到如下日志:
proceed====Date: Sat, 26 Aug 2017 08:02:59 GMT
Content-Type: application/json; charset=utf-8
可是日志顯示出來花來折欠,服務(wù)器端Spring的JSON接受都沒過就報(bào)空指針的事實(shí)還是擺在那里贝或。
苦逼的查了一番資料后,才發(fā)現(xiàn)一種解決方法锐秦,竟然需要我自己把JSON串寫好咪奖,通過RequestBody傳才可以,那真是一萬只草泥馬在心頭奔馳而過敖创病羊赵!
用于傳輸?shù)腟ervice自然也要做出對應(yīng)調(diào)整:
public interface RetrofitPostJsonService {
@POST("{action}")
Observable<ResponseBody> postResult(@Path("action") String action, @HeaderMap Map<String, String> headerParams, @Body RequestBody requestBody);
}
需要說明的一點(diǎn)是,大家如果與我之前發(fā)的POST傳輸?shù)腟ervice對比一下就會發(fā)現(xiàn)扇谣,這里要比之前少了“@FormUrlEncoded”的配置昧捷,那是因?yàn)檫@個(gè)配置信息本就是將URL格式化為為key-value格式,既然我們設(shè)置了要傳輸JSON格式罐寨,再用這個(gè)參數(shù)靡挥,自然是要報(bào)錯的:
Caused by: java.lang.IllegalArgumentException: @Body parameters cannot be used with form or multi-part encoding. (parameter #3)
這部分搞定了之后,該進(jìn)行的自然就是如何封裝到POST方法中了衩茸,對于這部分呢芹血,我這里總共分為了一下兩種情況:
- 普通key-Value形式的封裝
- 復(fù)雜JSON的封裝
普通key-Value形式的封裝###
把這一條放在最前面不僅僅是因?yàn)樗拿种杏衅胀▋蓚€(gè)字,顯得比較好處理楞慈,而是因?yàn)樗娴囊攘硪环N情況要好處理得多幔烛。
public <T extends BaseBean> void post(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
if (showDialog) {
showLoadDialog();
}
initBaseData(false);
if (jsonService == null) {
jsonService = retrofit.create(RetrofitPostJsonService.class);
}
if (params == null) {
params = new HashMap<>();
}
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
String.valueOf(new JSONObject(params)));
jsonService.postResult(action, headerParams, requestBody)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ResponseBody responseBody) {
hideLoadDialog();
try {
String responseString = responseBody.string();
LogUtil.info("responseString", action + "********** responseString post " + responseString);
callBack.success(action, new Gson().fromJson(responseString, clazz));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(@NonNull Throwable e) {
callBack.error(action, e);
}
@Override
public void onComplete() {
params = null;
}
});
}
可以看得出來,這段接受的方法囊蓝,實(shí)際上與之前封裝的POST方法真的卻別不大饿悬。
1、由于需要上傳JSON聚霜,且又是普通的Key-Value轉(zhuǎn)換而成的JSON狡恬,于是我們就可以實(shí)現(xiàn)最最簡單的方法實(shí)現(xiàn)珠叔,那就是直接將原本的之前封裝中用到的Map轉(zhuǎn)換成JSON就好。
String.valueOf(new JSONObject(params))
最外一層的目的是將得到的JSON轉(zhuǎn)換成String類型弟劲,用于上傳祷安。
2、前面說過了兔乞,Service已經(jīng)被改變了汇鞭,所以這里我們也需要將傳輸?shù)膮?shù)做出對應(yīng)修改,那就是創(chuàng)建一個(gè)RequestBody對象庸追,在配合上第一條生成的JSON霍骄,就是如下的代碼:
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
String.valueOf(new JSONObject(params)));
3、就是參數(shù)的傳遞淡溯,這個(gè)也是最簡單的了读整,那就是將我們的RequestBody 對象傳遞給Service中的postResult方法即可:
jsonService.postResult(action, headerParams, requestBody)
如此以來,我們終于可以成功的傳遞JSON了咱娶。
復(fù)雜JSON封裝###
上述方法已經(jīng)能夠?qū)崿F(xiàn)我們大多數(shù)接口的傳輸數(shù)據(jù)的需求米间,可是在大多數(shù),它也是有我們無法完成的部分豺总,哪怕是我下面列舉的簡單又常見的JSON形式:
[
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
}
]
傳輸?shù)木褪撬膫€(gè)學(xué)生的姓名车伞、年級择懂、以及班級喻喳,如果用之前的方法嘗試的話,我的嘗試的結(jié)果如下:
當(dāng)然困曙,除了直接創(chuàng)建JSON對象表伦,我們還可以創(chuàng)建一個(gè)GSON獲取json串,我就賤賤的嘗試了一下Gson解析:
List<BaseBean> beanList = new ArrayList<>();
for (int i = 0; i < 4; i++) {
BaseBean baseBean = new BaseBean();
baseBean.setName("name" + i);
baseBean.setMyClass("class" + i);
baseBean.setGrade("grade" + i);
beanList.add(baseBean);
}
Log.i("adada", new Gson().toJson(beanList).toString());
很榮幸的發(fā)現(xiàn)慷丽,竟然能夠轉(zhuǎn)換成功:
[
{
"grade": "grade0",
"myClass": "class0",
"name": "name0"
},
{
"grade": "grade1",
"myClass": "class1",
"name": "name1"
},
{
"grade": "grade2",
"myClass": "class2",
"name": "name2"
},
{
"grade": "grade3",
"myClass": "class3",
"name": "name3"
}
]
正興奮呢蹦哼,需求又變了一個(gè)樣子:
{
"student": [
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
}
],
"pass": [
{
"name": ""
},
{
"name": ""
}
]
}
剛剛看到的勝利曙光瞬間又陰云密布了,無奈之下要糊,就只能再創(chuàng)建一個(gè)傳輸?shù)腷ean:
class PostStudentBean{
private List<BaseBean> student;
private List<PassBean> pass;
public void setStudent(List<BaseBean> student){
this.student = student;
}
public List<BaseBean> getStudent(){
return student;
}
public void setPass(List<PassBean> pass){
this.pass= pass;
}
public List<PassBean> getPass(){
return pass;
}
}
class PassBean{
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
如此以來纲熏,就可以通過GSON將Bean轉(zhuǎn)換為JSON串,用于傳輸即可锄俄。
當(dāng)然局劲,上面用一種比較倒霉的方法說明了兩種我們在編程開發(fā)過程中可能會遇到的兩種情況(個(gè)人認(rèn)為如果需要Map配合Bean的不如就直接創(chuàng)建一個(gè)父級別的Bean好了),這部分操作暫時(shí)都是在創(chuàng)建參數(shù)的時(shí)候完成的奶赠,所以在這個(gè)第二種方法中鱼填,post傳參要比上一中方法少一個(gè)使用param傳遞參數(shù)的過程,卻需要通過創(chuàng)建Bean類毅戈,以及為Bean類賦值的步驟苹丸,同樣愤惰,需要重載post方法,多添加一個(gè)(String json)的參數(shù)赘理,然后將RequestBody 的創(chuàng)建改成如下形式即可:
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
這樣宦言,我們的就實(shí)現(xiàn)了Retrofit的JSON傳參,雖然看起來有些麻煩商模,但是……也確實(shí)麻煩蜡励,不過在有這方面需求的時(shí)候,這個(gè)封裝也是必不可少的過程阻桅,所以希望這里可以幫助到大家凉倚。
尾址特殊字符轉(zhuǎn)譯問題
這個(gè)內(nèi)容是在前面封裝的時(shí)候所沒有想到的,在開發(fā)過程中卻狠狠的坑了我一回嫂沉,除了含著淚解決稽寒,我們還有什么辦法呢!
之前我們的傳遞的內(nèi)容都很簡單趟章,比如我前面一直拿來舉例的豆瓣電影查詢杏糙,尾址只需要傳遞“top250”就算完成戰(zhàn)斗,所以也就沒有遇到過尾址特殊字符轉(zhuǎn)譯的問題蚓土,可是還是以豆瓣電影查詢?yōu)槔晔蹋暾埱骍RL為“https://api.douban.com/v2/movie/top250”,可是豆瓣API明顯不只是電影一項(xiàng):
[豆瓣書籍所搜] (https://api.douban.com/v2/book/search?q=python&fields=id,title)
[豆瓣音樂搜索] (https://api.douban.com/v2/music/search)
等等等等蜀漆,有興趣大家可以去豆瓣Api V2看一看還有多少谅河,這么多的內(nèi)容,我們在封裝網(wǎng)絡(luò)訪問框架的時(shí)候确丢,自然不會只使用“https://api.douban.com/v2/movie/”作為baseUrl绷耍,而通過觀察,顯而易見鲜侥,我們所使用的baseUrl將會是“https://api.douban.com/v2/”褂始,而后在根據(jù)具體需求設(shè)置尾址“movie/top250”、“book/search”描函、或者“music/search”,而當(dāng)如此使用的時(shí)候崎苗,我們今天遇到的問題也就來了,那就是尾址特殊字符轉(zhuǎn)譯舀寓,說具體點(diǎn)胆数,就是“/”被Retrofit進(jìn)行了轉(zhuǎn)譯,變成了“%2F”基公,當(dāng)然幅慌,除此之外還會有其他的特殊字符會被轉(zhuǎn)譯,具體請參見網(wǎng)址URL中特殊字符轉(zhuǎn)義編碼轰豆。查詢了一些資料胰伍,有一些解決方法是通過“//”齿诞,將被轉(zhuǎn)譯的字符再強(qiáng)行轉(zhuǎn)譯回來,原理嘛骂租,就好像傳說中的負(fù)負(fù)得正一樣祷杈。
只不過很不幸的是,我嘗試了這個(gè)方法之后渗饮,在上述監(jiān)聽到的URL卻編程了如下的樣子“https://api.douban.com/v2/movie%2F%2Ftop250”(點(diǎn)了一下但汞,豆瓣竟然能識別,不禁讓我“內(nèi)牛滿面”)互站,服務(wù)器果斷給我返回了404私蕾,查了N多資料,真心沒找到Retrofit應(yīng)該怎么解決這個(gè)問題胡桃,而之前使用Volley的時(shí)候踩叭,又沒遇到過這種情況。所以這里只能采用一個(gè)最無腦的解決方法翠胰,如果大家誰有更高端的解決方法容贝,希望能不吝賜教:
首先,將post或者get傳入的action根據(jù)action中最后一個(gè)“/”的坐標(biāo)將其分為兩部分之景,最后一個(gè)“/”后面的字符串被用做尾址斤富,繼續(xù)傳遞到后面Service對應(yīng)的方法中,也就是下面代碼中的“action1 ”锻狗,而其余的部分满力,也就是下面的“action2 ”則被用來當(dāng)做參數(shù)傳入Retrofit的初始化中,拼接到baseUrl中屋谭。
public <T extends BaseBean> void get(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
this.action = action;
if (showDialog) {
showLoadDialog();
}
String action1 = action.substring(action.lastIndexOf("/") + 1);
String action2 = action.substring(0, action.lastIndexOf("/") + 1);
initBaseData(action2, false);
if (getService == null) {
getService = retrofit.create(RetrofitGetService.class);
}
if (params == null) {
params = new HashMap<>();
}
getService.getResult(action1, headerParams, params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ResponseBody responseBody) {
hideLoadDialog();
try {
String responseString = responseBody.string();
LogUtil.info("responseString", action + "********** responseString get " + responseString);
callBack.success(action, new Gson().fromJson(responseString, clazz));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(@NonNull Throwable e) {
LogUtil.info("responseString", "responseString get " + e.toString());
callBack.error(action, e);
}
@Override
public void onComplete() {
params = null;
}
});
}
下面是動態(tài)拼接baseUrl的方法:
private void initBaseData(final String url, boolean isFromLogin) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS);
builder.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
LogUtil.info("zzz", "request====" + action);
LogUtil.info("zzz", "request====" + request.headers().toString());
LogUtil.info("zzz", "request====" + request.toString());
okhttp3.Response proceed = chain.proceed(request);
LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
return proceed;
}
});
Retrofit.Builder builder1 = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
builder1.baseUrl(baseUrl + url);
retrofit = builder1.build();
}
如此脚囊,就可以動態(tài)適配我們的尾址龟糕,同時(shí)也避免了轉(zhuǎn)譯帶來的影響桐磁,雖然說方法簡陋的可以,但是暫時(shí)拿來應(yīng)急還是可以考慮的讲岁,如果后續(xù)發(fā)現(xiàn)更好的方法我擂,我會繼續(xù)更新到后續(xù)的博客上的。