前言
最近在學習使用Retrofit,並嘗試將之引入到現有的專案中來。大家都知道,在Http請求中我們使用Content-Type
來指定不同格式的請求資訊:
APP_FORM_URLENCODED("application/x-www-form-urlencoded"),
APP_JSON("application/json"),
APP_OCTET_STREAM("application/octet-stream"),
MULTIPART_FORM_DATA("multipart/form-data"),
TEXT_HTML("text/html"),
TEXT_PLAIN("text/plain"),
複製程式碼
實際專案中通常最後的請求引數都包含預設的一些引數(Token,Api版本、App版本等)和普通的請求引數。網上有很多關於第一種Content-Type
新增預設引數的方法(post-表單)。而在我現有專案上,除檔案上傳外絕大多數請求都走了post-json的方式。這裡暫不討論兩者的優缺點,而是談下Content-Type
為application/json
時,如何去優雅地新增預設引數。
傳統方式:
我們先來回憶下post-json的兩種方式
public interface Apis {
@POST("user/login")
Observable<Entity<User>> login(@Body RequestBody body);//構造一個RequestBody物件
@POST("user/login")
Observable<Entity<User>> login(@Body LoginInfo loginInfo);//構造一個實體物件
}
複製程式碼
第二種方法,你需要為每一個不同的請求的物件建立一個不同的Model,太麻煩了,這裡選擇第一種直接構造RequestBody物件:
Retrofit mRetrofit = new Retrofit.Builder()
.baseUrl(HttpConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())//新增gson轉換器
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//新增rxjava轉換器
.client(new OkHttpClient.Builder().build())
.build();
Apis mAPIFunction = mRetrofit.create(Apis.class);
Map<String, Object> params = new LinkedHashMap<>();
params.put("name", "吳彥祖");
params.put("request", "123456");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JsonHelper.toJSONString(params));
mAPIFunction.login(RequestBody.create(requestBody))
複製程式碼
執行後通過抓包檢視,請求體如下:
而我希望的結果是這樣的:
當然我們可以每次構造RequestBody
,在傳入的引數中加入預設引數:
public static RequestBody getRequestBody(HashMap<String, Object> hashMap) {
Map<String, Object> params = new LinkedHashMap<>();
params.put("auth", getBaseParams());
params.put("request", hashMap);
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JsonHelper.toJSONString(params));
}
複製程式碼
這樣完全沒問題,但不夠優雅,所以接下來我們來討論我所想到的一種方式
攔截器方式:
哈哈,相信熟悉OkHttp的同學已經想到這種方式了,是的很多網上關於第一種Content-Type
新增預設引數也是這麼做的(原文連結):
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (request.method().equals("POST")) {
if (request.body() instanceof FormBody) {
FormBody.Builder bodyBuilder = new FormBody.Builder();
FormBody formBody = (FormBody) request.body();
//把原來的引數新增到新的構造器,(因為沒找到直接新增,所以就new新的)
for (int i = 0; i < formBody.size(); i++) {
bodyBuilder.addEncoded(formBody.encodedName(i), formBody.encodedValue(i));
}
formBody = bodyBuilder
.addEncoded("clienttype", "1")
.addEncoded("imei", "imei")
.addEncoded("version", "VersionName")
.addEncoded("timestamp", String.valueOf(System.currentTimeMillis()))
.build();
request = request.newBuilder().post(formBody).build();
}
return chain.proceed(request);
}
複製程式碼
在上面,我們拿到了request物件,然後拿到了requestBody物件,然後
判斷是不是FormBody型別,如果是的話,將裡面的鍵值對取出,並新增預設引數的鍵值對並構造出一個新的formBody物件,最後將原來用request物件構造出新的一個request物件,將新的formBody物件穿進去,攔截器返回。formBody物件是Content-Type
為application/x-www-form-urlencoded
時,Retrofit為我們生成的物件,它是RequestBody的子類;而Content-Type
為application/json
時,生成的就是RequestBody
(準確的說是匿名子類)。所以我們只要繼承重寫RequestBody
,記錄請求內容,再將它在攔截器裡取出加入並處理就行了。
public class PostJsonBody extends RequestBody {
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final Charset charset = Util.UTF_8;
private String content;
public PostJsonBody(@NonNull String content) {
this.content = content;
}
public String getContent() {
return content;
}
@Nullable
@Override
public MediaType contentType() {
return JSON;
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
byte[] bytes = content.getBytes(charset);
if (bytes == null) throw new NullPointerException("content == null");
Util.checkOffsetAndCount(bytes.length, 0, bytes.length);
sink.write(bytes, 0, bytes.length);
}
public static RequestBody create(@NonNull String content) {
return new PostJsonBody(content);
}
}
複製程式碼
攔截器裡面取出原始json資料,並新增新的預設引數:
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
if (originalRequest.method().equals("POST")) {
RequestBody requestBody = originalRequest.body();
if (requestBody instanceof PostJsonBody) {
String content = ((PostJsonBody) requestBody).getContent();
HashMap<String, Object> hashMap = JsonHelper.fromJson(content, HashMap.class);
builder.post(RequestBodyFactory.getRequestBody(hashMap));
}
}
return chain.proceed(builder.build());
}
複製程式碼
這樣在外面我們只要改動一行程式碼就可以實現全域性新增預設引數:
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json;charset=utf-8"),JsonHelper.toJSONString(params));
複製程式碼
替換為:
RequestBody requestBody = PostJsonBody.create( JsonHelper.toJSONString(params));
複製程式碼
不成熟的想法:
先來回顧下post-表單的用法,我們將前面的登陸改用post-表單定義如下:
@FormUrlEncoded //@FormUrlEncoded將會自動將請求引數的`content
@POST("user/login")
Observable<Entity<User>> login(@Field("account") String name, @Field("password") String password);
複製程式碼
相比post-json的api定義是不是一目瞭然多了?而且不需要在呼叫介面的時候自己構造HashMap
和requestBody
,請求方法裡面定義的引數和我們從輸入框中拿到的原始資料已經一一對應,直接呼叫就好了:
mAPIFunction.login("吳彥祖","123456");
複製程式碼
但缺點上面也說了,post-表單的請求體裡面只能有鍵值對引數,不能描述更復雜的物件,如果統一在攔截器裡面加入了預設引數也只能和普通引數混在一起:
所以能不能結合兩者優點呢?如果普通請求引數是鍵值對引數的話,這是可以的,原理就是在Api介面定義的時候定義為post-表單,然後在OkHttp攔截器裡面加入預設引數,並將post-表單請求改為post-json請求(get請求也是同樣的方法):public class AddQueryParameterInterceptor implements Interceptor {
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder();
if (originalRequest.method().equals("POST")) {
RequestBody requestBody = originalRequest.body();
if (requestBody instanceof PostJsonBody) {
String content = ((PostJsonBody) requestBody).getContent();
HashMap<String, Object> hashMap = JsonHelper.fromJson(content, HashMap.class);
builder.post(RequestBodyFactory.getRequestBody(hashMap));
} else if (requestBody instanceof FormBody) {
FormBody formBody = (FormBody) requestBody;
LinkedHashMap<String, Object> hashMap = new LinkedHashMap<>();
for (int i = 0; i < formBody.size(); i++) {
hashMap.put(formBody.encodedName(i), formBody.encodedValue(i));
}
builder.post(RequestBodyFactory.getRequestBody(hashMap));
}
}
return chain.proceed(builder.build());
}
}
複製程式碼
如果你原有專案裡面有很多get請求或者post-表單請求的介面,想把他們統一改成post-json方式的話,這是一種不用動上層的簡單方法。