HttpClient时代

作为深受Apache HttpClient毒害的一代青年,不得不吐槽HttpClient的版本维护和API文档有多糟糕。诟病缠身的HttpClient3.x4.xapi变更面目全非,甚至4.0-4.5api改动也不少。如果你以前使用3.x,升级到4.0后,http代码几乎全改了。大家可以看看Apache官网看看httpClient发布历史(3.x历史4.x历史)。文档嘛,Apache官网简直....连程序猿这审美观都不想看!

 

HttpClient发展历史相当长,最早是2001.10发布2.0-alpha 12004.11发布3.0-beta12008.1发布4.0-beta1,直到2012.2才发布4.2-beta12014.12发布4.4-release2016.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-rc12016.1发布3.0,更新速度相当快,而且开发人员经常对代码进行维护,看看http://square.github.io/okhttp就知道了。相比之下,httpClient维护相当糟糕。

 

Api文档方面,我非常喜欢Square公司的设计风格,okHttp首页相当简洁,OverviewExampleDownload全在首页展示,详细使用案例、说明,在github上很清晰。

 

Retrofit

 

从发布历史上来看,Retrofitokhttp是兄弟,Square公司在2013.5.13发布1.02015.8发布2.0-beta1

 

Retrofit底层基于OkHttp·,并且可以加很多Square开发的周边产品converter-gsonadapter-rxjava等。Retrofit抱着gson&rxjava的大腿,这种聪明做法,也是最近大受欢迎的原因之一,所谓“Rxjava火了,Retrofit也火了Retrofit·不仅仅支持这两种周边,我们可以自定义converter&call adapter,可以你喜欢的其他第三方库。

 

 

 

介绍了主流java http请求库历史,大家对为什么用retrofit”有个印象了吧?想想,如果没有Square公司,apahce httpClient还将毒害多少无知青年。

 

 

 

何为非Restful Api?

 

Restful Api

 

User数据,有uidnameRestful Api返回数据:

 

{

    "name": "kkmike999",

    "uid": 1

}

 

在数据库没找到User,直接返回错误的http code。但弊端是当在浏览器调试api,后端查询出错时,很难查看错误码&错误信息。(当然用chrome的开发者工具可以看,但麻烦)

 

Not Restful Api

 

但不少后端工程师,并不一定喜欢用Restful Api,他们会自己在json中加入retmsg这种数据。当User正确返回:

 

{

    "ret": 0,

    "msg": "成功",

    "data": {

        "uid": 1,

        "name": "kkmike999"

    }

}

 

错误返回:

 

{

    "ret": -1,

    "msg": "失败"

}

 

这样的好处,就是调试api方便,在任意浏览器都可以直观地看到错误码&错误信息。

 

 

 

 

Retrofit一般用法

 

本来Retrofitrestful的支持,可以让我们写少很多冤枉代码。但后端这么搞一套,前端怎么玩呀?既然木已成舟,我们做APP的总不能老对后端指手画脚,友谊小船说翻就翻。

 

先说说retrofit普通用法

 

public class User {

    int    uid;

    String name;

}

public interface UserService {

 

    @GET("not_restful/user/{name}.json")

    Call loadUser(@Path("name") String name);

}

 

BeanService准备好,接下来就是调用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,直接返回Userjson,那调用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 responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

      TypeAdapter adapter = gson.getAdapter(TypeToken.get(type));

      return new GsonResponseBodyConverter<>(gson, adapter);

  }

 

  @Override

  public Converter 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 implements Converter {

    private final Gson           gson;

    private final TypeAdapter adapter;

 

    GsonResponseBodyConverter(Gson gson, TypeAdapter 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是什么。TypeAdaptergson让使用者自定义解析的jsonTypeservice方法返回值Call的泛型类型。UserServiceCall loadUser(...),泛型参数是User,所以type就是User类型。

 

重写GsonResponseConverter

 

由源码看出,是GsonResponseBodyConverterjson进行解析的,只要重写GsonResponseBodyConverter,自定义解析,就能达到我们目的了。

 

GsonResponseBodyConverterGsonConverterFactory都是final class,并不能重写。靠~ 不让重写,我就copy代码!

 

新建retrofit2.converter.gson目录,新建CustomConverterFactory,把GsonConverterFactory源码拷贝过去,同时新建CustomResponseConverter CustomConverterFactoryGsonResponseBodyConverter替换成CustomResponseConverter

 

public final class CustomConverterFactory extends Converter.Factory {

    ......

    

    @Override

    public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

        TypeAdapter adapter = gson.getAdapter(TypeToken.get(type));

        return new CustomResponseConverter<>(gson, adapter);

    }

    ......

}

 

CustomResponseConverter

 

public class CustomResponseConverter implements Converter {

 

    private final Gson gson;

    private final TypeAdapter adapter;

 

    public CustomResponseConverter(Gson gson, TypeAdapter 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,所以CustomConverterFactoryimport 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 loadUser(@Path("name") String name);

 

修改CustomResponseConverter

 

public class CustomResponseConverter implements Converter {

 

    private final Gson gson;

    private final TypeAdapter adapter;

    private final String name;

 

    public CustomResponseConverter(Gson gson, TypeAdapter 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);

                }

                ...

    }

}

 

CustomConverterFactoryresponseBodyConverter(...)加上

 

@Overridepublic Converter 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责任太重,又是判断retmsg,又是解析json数据并返回bean,如果遇到奇葩jsonCustomResponseConverter远远不够强大,而且不灵活。

 

怎么办,干嘛不自定义converter呢?

问题来了,这个converter应该如何传给CustomConverterFactory?因为在new Retrofit.Builder().addConvertFactory(…)时就要添加ConverterFactory,那时并不知道返回json是怎样,哪个service要用哪个adapter。反正通过构造方法给CustomConverterFactoryConverter肯定行不通。

 

我们上面不是用过Annotaion吗?同样手段再玩一把如何。写一个@Converter注解:

 

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Converter {

 

    Class converter();

}

 

并且写一个Converter抽象类:

 

public abstract class AbstractResponseConverter implements Converter{

 

    protected Gson gson;

 

    public AbstractResponseConverter(Gson gson) {

        this.gson = gson;

    }

}

 

为什么要写一个继承Converter抽象类?让我们自定义的Converter直接继承Converter不行吗?

注意了,@Adapter只能携带Classint``String等基本类型,并不能带converter对象。而我们需要CustomConverterFactoryresponseBodyConverter()方法中,通过反射,new一个converter对象,而CustomConverterFactory并不知道调用Converter哪个构造函数,传什么参数。所以,干脆就写一个AbstractResponseConverter,让子类继承它,实现固定的构造方法。这样CustomConverterFactory就可以获取固定的构造方法,生成Converter对象并传入如gson``typeAdapter参数了。

 

public class ClazzInfo{

    List students;

    Info     info;

}

public class ClassConverter implements AbstractResponseConverter{

 

    public ClassConverter(Gson gson){

        super(gson);

    }

 

    @Override

    public ClazzInfo convert(ResponseBody value) throws IOException {

        // 这里你想怎么解析json就怎么解析啦

        ClazzInfo clazz = ...

        return clazz;

    }

}

 

    @Override

    public Converter responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {

 

        for (Annotation annotation : annotations) {

            if (annotation instanceof Converter) {

                try {

                    Class converterClazz = ((Converter) annotation). converter();

                    // 获取有 以gson参数的 构造函数

                    Constructor 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 loadClass(@Path("cid") String cid);

 

写到这里,已经快吐血了。怎么会有这么奇葩的后端.... 正常情况下,应该把"users""class"封装在"data"里,这样我们就可以直接把返回结果写成Call就可以了。

 

 

 

小结

 

Retrofit可以大量减少写无谓的代码,减少工作量之余,还能让http层更加清晰、解耦。当你遇到非Restful Api时,应该跟后端协商一种固定的json格式,便于APP写代码。

 

 

代码越少,错得越少

 

 

同时,使用Retrofit让你更容易写单元测试。由于Retrofit基于okhttp,完全不依赖android库,所以可以用junit直接进行单元测试,而不需要robolectric或者在真机、模拟器上运行单元测试。之后有空我会写关于Android单元测试的文章。

 

 

“我们可以相信的变革”( CHANGE WE CAN BELIEVE IN )          ——美国总统第44任总统,奥巴马

 

 

如果你还用httpClient,请尽管大胆尝试Retrofitdon't afraid change,绝对给你意想不到的惊喜!并希望作为开发者的你,受此启发,写出更加灵活的代码,顺便分享下自己的Android技术交流群:259658256

你可能感兴趣的:(HttpClient时代)