毒害一代Java程序猿的HttpClient


前言

2016年以來,越來越多Android開發者使用Retrofit作為HTTP請求框架。原因其一,Google發布Android 6.0 SDK (API 23) 拋棄了HttpClient;其二,Square在2016.1.2發布okhttp3.0、2016.3.11正式發布Retrofit 2.0。

 

HttpClient時代

作為深受Apache HttpClient毒害的一代青年,不得不吐槽HttpClient的版本維護和API文檔有多糟糕。詬病纏身的HttpClient從3.x到4.x,api變更面目全非,甚至4.0-4.5,api改動也不少。如果你以前使用3.x,升級到4.0后,http代碼幾乎全改了。大家可以看看Apache官網看看httpClient發布歷史(3.x歷史4.x歷史)。文檔嘛,Apache官網簡直....連程序猿這審美觀都不想看!HttpClient發展歷史相當長,最早是2001.10發布2.0-alpha 1,2004.11發布3.0-beta1,2008.1發布4.0-beta1,直到2012.2才發布4.2-beta1,2014.12發布4.4-release,2016.1發布5.0-alpha。由於源遠流長,httpClient在國人心中根心蒂固。可以想象當年讀書(也就4年前嘻嘻^_^),翻牆未普及,天朝百度蠻橫,搜“java http請求”出來的幾乎都是httpClient(不信你現在百度)。2013年以來,Google逐漸意識到httpClient的詬病,狠心之下,拋棄httpClient,因為我們有更好的選擇:okhttp.

 

OkHttp

美國移動支付公司Square,在2013.5.6開源一款 java http請求框架——OkHttp. 發布之后,在國外迅速流行起來,一方面是httpClient太繁瑣、更新慢,另一方面okHttp確實好用。okHttp發布之后不斷地改進,2014.5發布2.0-rc1,2016.1發布3.0,更新速度相當快,而且開發人員經常對代碼進行維護,看看http://square.github.io/okhttp就知道了。相比之下,httpClient維護相當糟糕。Api文檔方面,我非常喜歡Square公司的設計風格,okHttp首頁相當簡潔,Overview、Example、Download全在首頁展示,詳細使用案例、說明,在github上很清晰。

 

Retrofit

從發布歷史上來看,Retrofit和okhttp是兄弟,Square公司在2013.5.13發布1.0,2015.8發布2.0-beta1。Retrofit底層基於OkHttp·,並且可以加很多Square開發的“周邊產品”:converter-gson、adapter-rxjava等。Retrofit抱着gson&rxjava的大腿,這種聰明做法,也是最近大受歡迎的原因之一,所謂“Rxjava火了,Retrofit也火了”。Retrofit·不僅僅支持這兩種周邊,我們可以自定義converter&call adapter,可以你喜歡的其他第三方庫。介紹了主流java http請求庫歷史,大家對“為什么用retrofit”有個印象了吧?想想,如果沒有Square公司,apahce httpClient還將毒害多少無知青年。

 

何為非Restful Api?

Restful Api

User數據,有uid、name,Restful Api返回數據:

{
    "name": "kkmike999",
    "uid": 1
}

在數據庫沒找到User,直接返回錯誤的http code。但弊端是當在瀏覽器調試api,后端查詢出錯時,很難查看錯誤碼&錯誤信息。(當然用chrome的開發者工具可以看,但麻煩)

Not Restful Api

但不少后端工程師,並不一定喜歡用Restful Api,他們會自己在json中加入ret、msg這種數據。當User正確返回:

{
    "ret": 0,
    "msg": "成功",
    "data": {
         "uid": 1,
        "name": "kkmike999"
    }
}

錯誤返回:

{
    "ret": -1,
    "msg": "失敗"
}

這樣的好處,就是調試api方便,在任意瀏覽器都可以直觀地看到錯誤碼&錯誤信息。還有一個例子,百度地圖Web api

 

Retrofit一般用法

本來Retrofit對restful的支持,可以讓我們寫少很多冤枉代碼。但后端這么搞一套,前端怎么玩呀?既然木已成舟,我們做APP的總不能老對后端指手畫腳,友誼小船說翻就翻。

先說說retrofit普通用法

public class User {
    int    uid;
    String name;
}

public interface UserService {
    @GET("not_restful/user/{name}.json")
    Call<User> loadUser(@Path("name") String name);

}

//Bean和Service准備好,接下來就是調用Retrofit了:
OkHttpClient client = new OkHttpClient.Builder().build();
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://***.b0.upaiyun.com/")
                                          .addConverterFactory(GsonConverterFactory.create())
                                          .client(client)
                                          .build();

UserService userService = retrofit.create(UserService.class);

User user = userService.loadUser("kkmike999")
                   .execute()
                   .body();

此處加入了GsonConverterFactory,沒有使用RxJavaCallAdapter。如果是restful api,直接返回User的json,那調用execute().body()就能獲得正確的User了。然而,not restful api,返回一個不正確的User ,也不拋錯,挺難堪的。

 

ResponseConverter

我們留意到GsonConverterFactory,看看源碼:

package retrofit2.converter.gson;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit; 

public final class GsonConverterFactory extends Converter.Factory {

  public static GsonConverterFactory create() {
      return create(new Gson());
  }
  public static GsonConverterFactory create(Gson gson) {
      return new GsonConverterFactory(gson);
  }
  private final Gson gson;
  private GsonConverterFactory(Gson gson) {
      if (gson == null) throw new NullPointerException("gson == null");
      this.gson = gson;
  }

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
      TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
     return new GsonResponseBodyConverter<>(gson, adapter);
  }

  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
      Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
     return new GsonRequestBodyConverter<>(gson, adapter);
  }
}

//responseBodyConverter方法返回GsonResponseBodyConverter,我們再看看GsonResponseBodyConverter源碼: package retrofit2.converter.gson;
final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
    private final Gson           gson;
    private final TypeAdapter<T> adapter;
    GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
        this.gson = gson;
        this.adapter = adapter;
    }
    @Override
    public T convert(ResponseBody value) throws IOException {
        JsonReader jsonReader = gson.newJsonReader(value.charStream());
        try {
            return adapter.read(jsonReader);
        } finally {
            value.close();
        }
    }
}

先給大家科普下,TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); 這里TypeAdapter是什么。TypeAdapter是gson讓使用者自定義解析的json,Type是service方法返回值Call<?>的泛型類型。UserService中Call<User> loadUser(...),泛型參數是User,所以type就是User類型。詳細用法參考:你真的會用Gson嗎?Gson使用指南(四)

 

重寫GsonResponseConverter

由源碼看出,是GsonResponseBodyConverter對json進行解析的,只要重寫GsonResponseBodyConverter,自定義解析,就能達到我們目的了。

但GsonResponseBodyConverter和GsonConverterFactory都是final class,並不能重寫。靠~ 不讓重寫,我就copy代碼!

新建retrofit2.converter.gson目錄,新建CustomConverterFactory,把GsonConverterFactory源碼拷貝過去,同時新建CustomResponseConverter。 把CustomConverterFactory的GsonResponseBodyConverter替換成CustomResponseConverter:

public final class CustomConverterFactory extends Converter.Factory {
     ......
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
        return new CustomResponseConverter<>(gson, adapter);
    }
    .....
}
寫CustomResponseConverter:
public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final TypeAdapter<T> adapter;
    public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter) {
       this.gson = gson;
       this.adapter = adapter;
    }
    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            String body = value.string();
            JSONObject json = new JSONObject(body);
            int    ret = json.optInt("ret");
            String msg = json.optString("msg", "");
            if (ret == 0) {
                if (json.has("data")) {
                    Object data = json.get("data");
                    body = data.toString();
                    return adapter.fromJson(body);
                } else {
                    return (T) msg;
                }
            } else {
                throw new RuntimeException(msg);
            }
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            value.close();
        }
    }
}

為什么我們要新建retrofit2.converter.gson目錄?因為GsonRequestBodyConverter不是public class,所以CustomConverterFactory要import GsonRequestBodyConverter就得在同一目錄下。當然你喜歡放在自己目錄下,可以拷貝源碼如法炮制。接下來,只要 new Retrofit.Builder().addConverterFactory(CustomConverterFactory.create())就大功告成了!

 

更靈活的寫法

上述做法,我們僅僅踏入半條腿進門,為什么?萬一后端不喜歡全用"data",而是根據返回數據類型命名,例如返回User用"user",返回Student用"student"呢?

{
    "ret": 0,
    "msg": "成功",
    "user": {
        "uid": 1,
        "name": "小明"
    }
}
{
    "ret": 0,
    "msg": "成功",
    "student": {
        "uid": 1,
        "name": "小紅"
    }
}

(此時是否有打死后端工程師的沖動?)

別怒,魔高一尺,道高一丈。

玩轉Service注解

既然retrofit能“理解”service方法中的注解,我們為何不試試?GsonConverterFactory的方法responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit),這里有Annotation[],沒錯,這就是service方法中的注解。

我們寫一個@Data注解類:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Data {
    String value() default "data";
}
//在loadUser(...)添加@Data:
@Data("user")
@GET("not_restful/user/{name}.json")
Call<User> loadUser(@Path("name") String name);
//修改CustomResponseConverter
public class CustomResponseConverter<T> implements Converter<ResponseBody, T> {
    private final Gson gson;
    private final TypeAdapter<T> adapter;
    private final String name;

    public CustomResponseConverter(Gson gson, TypeAdapter<T> adapter, String name) {
        this.gson = gson;
        this.adapter = adapter;
        this.name = name;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        try {
            ...
            if (ret == 0) {
                if (json.has(name)) {
                    Object data = json.get(name);
                    body = data.toString();
                    return adapter.fromJson(body);
                }
                ...
    }
}

// 給CustomConverterFactory的responseBodyConverter(...)加上
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    String name = "data";// 默認"data"
    for (Annotation annotation : annotations) {
        if (annotation instanceof Data) {
            name = ((Data) annotation).value();
            break;
        }
    }
    ...
    return new CustomResponseConverter<>(gson, adapter, name);
}

這么寫后,后端改什么名稱都不怕!

 

更靈活的Converter

有個需求:APP顯示某班級信息&學生信息。后台拍拍腦袋:

{
    "ret": 0,
    "msg": "",
    "users": [
        {
            "name": "鳴人",
            "uid": 1
        },
        {
            "name": "佐助",
            "uid": 2
        }
    ],
    "info": {
        "cid": 7,
        "name": "第七班"
    }
}

哭了吧,滅了后端工程師恐怕也難解心頭之恨!阿尼陀佛, 我不是說了嗎?魔高又一尺,道又高一丈。我們意識到,CustomResponseConverter責任太重,又是判斷ret、msg,又是解析json數據並返回bean,如果遇到奇葩json,CustomResponseConverter遠遠不夠強大,而且不靈活。怎么辦,干嘛不自定義converter呢?問題來了,這個converter應該如何傳給CustomConverterFactory?因為在new Retrofit.Builder().addConvertFactory(…)時就要添加ConverterFactory,那時並不知道返回json是怎樣,哪個service要用哪個adapter。反正通過構造方法給CustomConverterFactory傳Converter肯定行不通。

我們上面不是用過Annotaion嗎?同樣手段再玩一把如何。寫一個@Converter注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Converter {
    Class<? extends AbstractResponseConverter> converter();

}

//並且寫一個Converter抽象類:
public abstract class AbstractResponseConverter<T> implements Converter<ResponseBody, T>{
    protected Gson gson;
    public AbstractResponseConverter(Gson gson) {
     this.gson = gson;
    }
}

為什么要寫一個繼承Converter抽象類?讓我們自定義的Converter直接繼承Converter不行嗎?
注意了,@Adapter只能攜帶Class<?>和int``String等基本類型,並不能帶converter對象。而我們需要CustomConverterFactory在responseBodyConverter()方法中,通過反射,new一個converter對象,而CustomConverterFactory並不知道調用Converter哪個構造函數,傳什么參數。所以,干脆就寫一個AbstractResponseConverter,讓子類繼承它,實現固定的構造方法。這樣CustomConverterFactory就可以獲取固定的構造方法,生成Converter對象並傳入如gson``typeAdapter參數了。

public class ClazzInfo{
    List<Student> students;
    Info  info;
}

public class ClassConverter implements AbstractResponseConverter<ClazzInfo>{
    public ClassConverter(Gson gson){
        super(gson);
    }
    @Override
    public ClazzInfo convert(ResponseBody value) throws IOException {
        // 這里你想怎么解析json就怎么解析啦
        ClazzInfo clazz = ...
        return clazz;
    }
}

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        for (Annotation annotation : annotations) {
           if (annotation instanceof Converter) {
                try {
                    Class<? extends AbstractResponseConverter> converterClazz = ((Converter) annotation). converter();
                    // 獲取有 以gson參數的 構造函數
                    Constructor<? extends AbstractResponseConverter> constructor = converterClazz .getConstructor(Gson.class);
                    AbstractResponseConverter  converter = constructor.newInstance(gson);
                    return converter;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        ...
        return new CustomResponseConverter<>(gson, adapter, name);
    }

Service方法注解:

@Converter(converter = ClassConverter.class)

@GET("not_restful/class/{cid}.json")

Call<ClazzInfo> loadClass(@Path("cid") String cid);

正常情況下,應該把"users"和"class"封裝在"data"里,這樣我們就可以直接把返回結果寫成Call<ClassInfo>就可以了。

 

小結

Retrofit可以大量減少寫無謂的代碼,減少工作量之余,還能讓http層更加清晰、解耦。當你遇到非Restful Api時,應該跟后端協商一種固定的json格式,便於APP寫代碼。

代碼越少,錯得越少同時,使用Retrofit讓你更容易寫單元測試。由於Retrofit基於okhttp,完全不依賴android庫,所以可以用junit直接進行單元測試,而不需要robolectric或者在真機、模擬器上運行單元測試。之后有空我會寫關於Android單元測試的文章。“我們可以相信的變革”( CHANGE WE CAN BELIEVE IN ) ——美國總統第44任總統,奧巴馬

如果你還用httpClient,請盡管大膽嘗試Retrofit,don't afraid change,絕對給你意想不到的驚喜!並希望作為開發者的你,受此啟發,寫出更加靈活的代碼。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM