毒害一代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年前嘻嘻^_^),FQ未普及,天朝百度蛮横,搜“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,绝对给你意想不到的惊喜!并希望作为开发者的你,受此启发,写出更加灵活的代码。


时间: 2024-08-29 16:35:42

毒害一代Java程序猿的HttpClient的相关文章

Java程序猿学习当中各个阶段的建议

回答阿里社招面试如何准备,顺便谈谈对于Java程序猿学习当中各个阶段的建议 引言 其实本来真的没打算写这篇文章,主要是LZ得记忆力不是很好,不像一些记忆力强的人,面试完以后,几乎能把自己和面试官的对话都给记下来.LZ自己当初面试完以后,除了记住一些聊过的知识点以外,具体的内容基本上忘得一干二净,所以写这篇文章其实是很有难度的. 但是,最近问LZ的人实在是太多了,为了避免重复回答,给自己省点力气,干脆就在这里统一回复了. 其实之前LZ写过一篇文章,但是那篇文章更多的是在讨论“面试前该不该刷题”这个

[转] java书籍(给Java程序猿们推荐一些值得一看的好书 + 7本免费的Java电子书和教程 )

7本免费的Java电子书和教程 1. Thinking in Java (Third Edition) 本书的作者是Bruce Eckel,它一直都是Java最畅销的免费电子书.这本书可以帮助你系统的学习Java,里面包含有很多好的代码示例.第三版仍旧是免费的,直到第四版才开始收费,不过仍旧值得买一本收藏. Think in Java 免费下载: Thinking in Java 2. The Java Tutorials 这个教程来自于Oracle/Sun.对于初学者是不错的选择.我们可以根据

Java程序猿修炼之道 之 Logging(3/3) - 怎么分析Log

1. 说明 作为一个程序猿我们常常要做一件事情:获取某个Log文件,从当中找出自己想要的信息. 本文总结了我在工作中使用了哪些工具来分析Log文件获取我想要的信息,我近期几年的工作环境都是server在Linux上,工作机是Windows, 所以我用的工具主要是Linux上的一些命令行工具,当然他们在Windows上也能够用,详细看以下的工具篇. 先声明,我仅仅是很普通的Linux用户,所以假设有些Linux命令或者工具用的不高效,请大家教育我. 题外话:我的观点是作为一个Java程序猿,仅仅须

一篇让Java程序猿随时可以翻看的Oracle总结

一篇让Java程序猿随时可以翻看的Oracle总结 前言:Oracle学习也有十几天了,但是呢,接下来还要学习许多其他的东西,并不能提步不前,所以在此总结了以下Oracle中常用的命令和语句,没有语法都是实例,以便以后工作的时候随时翻看,毕竟是自己的东西,一看就懂.  有关的语句和操作基本都是按照实战中的顺序来总结的,比如创建用户,建表,序列初始化,插入数据的顺序呢. 这篇文章的基表是大家最为熟知的Scott用户下的emp员工表,dept部门表以及salgrade薪水等级表,一切的语句都是围绕它

JAVA程序猿怎么才干高速查找到学习资料?

JAVA程序猿怎么才干高速查找到学习资料? JAVA学习资料在互联网上较为零散,并且大多是英文的.以下介绍3种方式,让程序猿能够高速地找到自己想要的资料. 一.导航站点: 有非常多类似hao123的站点,整合了非常多程序相关的资源,比如有一个"精简导航",依照语言的分类,整合了非常多的学习资源,网址是http://www.brieftools.info/ ,我们仅仅须要进入导航站,找到JAVA相应的资源就可以.导航上还有非常多其它语言的资源,假设后期准备学站点相关的东西,导航上面也有相

《Java程序猿面试笔试宝典》之字符串创建与存储的机制是什么

在Java语言中.字符串起着非常关键的数据.字符串的声明与初始化主要有例如以下两种情况:(1) 对于String s1=new String("abc")语句与String s2=new String("abc")语句,存在两个引用对象s1.s2,两个内容同样的字符串对象"abc".它们在内存中的地址是不同的.仅仅要用到new总会生成新的对象. (2) 对于String s1 = "abc"语句与String s2 = &qu

人在囧途——Java程序猿学习Python

引言 LZ之前其实一直对python都很好奇,只是苦于平时没有时间去了解它,因此趁着51假期这个机会,便迫不及待的开始了自己的探索.作为一个标准的Java程序猿,在了解python的过程当中,LZ遇到了很多囧事,接下来LZ就一一给大家说道说道.本文纯属看个乐子,非python教学. 囧事一:eclipse插件安装篇 由于LZ习惯了使用eclipse进行开发,因此对python的研究,还是希望可以在eclipse上进行试验.那么第一件事,自然是安装python的eclipse插件,于是百度.goo

给Java程序猿们推荐一些值得一看的好书

学习的最好途径就是看书 "学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 1.能出版出来的书一定是经过反复的思考.雕琢和审核的,因此从专业性的角度来说,一本好书的价值远超其他资料 2.对着书上的代码自己敲的时候方便 "看完书之后再次提升自我的最好途径是看一些相关的好博文",我个人认为这是学习的第二部,因为好的博文往往是自己学习之后的一些总结和提炼,对于梳理学习的内容很有好处,当然这里不是说自己的学习方法,就不再扯

Java程序猿的JavaScript学习笔记(1——理念)

计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript学习笔记(3--this/call/apply) Java程序猿的JavaScript学习笔记(4--this/闭包/getter/setter) Java程序猿的JavaScript学习笔记(5--prototype) Java程序猿的JavaScript学习笔记(6--面向对象模拟) Java程