Java HTTP 组件库选型看这篇就够了

最近项目需要使用 Java 重度调用 HTTP API 接口,于是想着封装一个团队公用的 HTTP client lib. 这个库需要支持以下特性:

连接池管理,包括连接创建和超时、空闲连接数控制、每个 host 的连接数配置等。基本上,我们想要一个 go HTTP 标准库自带的连接吃管理功能。

域名解析控制。因为调用量会比较大,因此希望在域名解析这一层做一个调用端可控的负载均衡,同时可以对每个服务器 IP 进行失败率统计和健康度检查。

Form/JSON 调用支持良好。

支持同步和异步调用。

在 Java 生态中,虽然有数不清的 HTTP client lib 组件库,但是大体可以分为这三类:

JDK 自带的HttpURLConnection标准库;

Apache HttpComponents HttpClient, 以及基于该库的 wrapper, 如Unirest.

非基于 Apache HttpComponents HttpClient, 大量重写应用层代码的 HTTP client 组件库,典型代表是OkHttp.

HttpURLConnection

使用 HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。以发起一个 HTTP POST 请求为例:


importjava.io.BufferedReader;

importjava.io.InputStream;

importjava.io.InputStreamReader;

importjava.io.OutputStream;

importjava.net.HttpURLConnection;

importjava.net.URL;



publicclassHttpUrlConnectionDemo {


    publicstaticvoidmain(String[] args) throwsException {

        String urlString = "https://httpbin.org/post";

        String bodyString = "password=e10adc3949ba59abbe56e057f20f883e&username=test3";


        URL url = newURL(urlString);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        conn.setRequestMethod("POST");

        conn.setDoOutput(true);


        OutputStream os = conn.getOutputStream();

        os.write(bodyString.getBytes("utf-8"));

        os.flush();

        os.close();


        if(conn.getResponseCode() == HttpURLConnection.HTTP_OK) {

            InputStream is = conn.getInputStream();

            BufferedReader reader = newBufferedReader(newInputStreamReader(is));

            StringBuilder sb = newStringBuilder();

            String line;

            while((line = reader.readLine()) != null) {

                sb.append(line);

            }

            System.out.println("rsp:"+ sb.toString());

        } else{

            System.out.println("rsp code:"+ conn.getResponseCode());

        }

    }


}

可以看到,使用 HttpURLConnection 发起 HTTP 请求是比较原始(low level)的,基本上你可以理解为它就是对网络栈传输层(HTTP 一般为 TCP,HTTP over QUIC 是 UDP)进行了一次浅层次的封装,操作原语就是在打开的连接上面写请求 request 与读响应 response. 而且 HttpURLConnection 无法支持 HTTP/2. 显然,官方是知道这些问题的,因此在 Java 9 中,官方在标准库中引入了一个 high level、支持 HTTP/2 的 HttpClient. 这个库的接口封装就非常主流到位了,发起一个简单的 POST 请求:

1

HttpRequest request = HttpRequest.newBuilder()

  .uri(newURI("https://postman-echo.com/post"))

  .headers("Content-Type", "text/plain;charset=UTF-8")

  .POST(HttpRequest.BodyProcessor.fromString("Sample request body"))

  .build();

封装的最大特点是链式调用非常顺滑,支持连接管理等特性。但是这个库只能在 Java 9 及以后的版本使用,Java 9 和 Java 10 并不是 LTS 维护版本,而接下来的 Java 11 LTS 要在2018.09.25发布,应用到线上还需要等待一段时间。因此,虽然挺喜欢这个自带标准库(毕竟可以不引入三方依赖),但当前是无法在生产环境使用的。

Apache HttpComponents HttpClient

Apache HttpComponents HttpClient 的前身是Apache Commons HttpClient, 但是 Apache Commons HttpClient 已经停止开发,如果你还在使用它,请切换到 Apache HttpComponents HttpClient 上来。

Apache HttpComponents HttpClient 支持的特性非常丰富,完全覆盖我们的需求,使用起来也非常顺手:


importjava.io.ByteArrayOutputStream;

importjava.io.InputStream;

importjava.util.ArrayList;

importjava.util.HashMap;

importjava.util.List;

importjava.util.Map;


importorg.apache.commons.io.IOUtils;

importorg.apache.http.HttpResponse;

importorg.apache.http.NameValuePair;

importorg.apache.http.client.entity.UrlEncodedFormEntity;

importorg.apache.http.client.fluent.Request;

importorg.apache.http.client.methods.CloseableHttpResponse;

importorg.apache.http.client.methods.HttpPost;

importorg.apache.http.client.methods.RequestBuilder;

importorg.apache.http.impl.client.CloseableHttpClient;

importorg.apache.http.message.BasicNameValuePair;

importorg.apache.http.impl.client.HttpClients;



publicclassHttpComponentsDemo {

    finalstaticCloseableHttpClient client = HttpClients.createDefault();


    // 常规调用

    privateString sendPostForm(String url, Map<String, String> params) throwsException {

        HttpPost request = newHttpPost(url);


        // set header

        request.setHeader("X-Http-Demo", HttpComponentsDemo.class.getSimpleName());


        // set params

        if(params != null) {

            List<NameValuePair> nameValuePairList = newArrayList<NameValuePair>();

            for(Map.Entry<String, String> entry : params.entrySet()) {

                nameValuePairList.add(newBasicNameValuePair(entry.getKey(), entry.getValue()));

            }

            UrlEncodedFormEntity bodyEntity = newUrlEncodedFormEntity(nameValuePairList, "UTF-8");

            //System.out.println("body:" + IOUtils.toString(bodyEntity.getContent()));

            request.setEntity(newUrlEncodedFormEntity(nameValuePairList));

        }


        // send request

        CloseableHttpResponse response = client.execute(request);

        // read rsp code

        System.out.println("rsp code:"+ response.getStatusLine().getStatusCode());

        // return content

        String ret = readResponseContent(response.getEntity().getContent());

        response.close();

        returnret;

    }


    // fluent 链式调用

    privateString sendGet(String url) throwsException {

        returnRequest.Get(url)

                .connectTimeout(1000)

                .socketTimeout(1000)

                .execute().returnContent().asString();

    }


    privateString readResponseContent(InputStream inputStream) throwsException {

        if(inputStream == null) {

            return"";

        }

        ByteArrayOutputStream out = newByteArrayOutputStream();

        byte[] buf = newbyte[512];

        intlen;

        while(inputStream.available() > 0) {

            len = inputStream.read(buf);

            out.write(buf, 0, len);

        }


        returnout.toString();

    }


    publicstaticvoidmain(String[] args) throwsException {

        HttpComponentsDemo httpUrlConnectionDemo = newHttpComponentsDemo();

        String url = "https://httpbin.org/post";

        Map<String, String> params = newHashMap<String, String>();

        params.put("foo", "bar中文");

        String rsp = httpUrlConnectionDemo.sendPostForm(url, params);

        System.out.println("http post rsp:"+ rsp);


        url = "https://httpbin.org/get";

        System.out.println("http get rsp:"+ httpUrlConnectionDemo.sendGet(url));

    }

}

对 Client 细致的配置和自定义支持也是非常到位的:


// Create a connection manager with custom configuration.

PoolingHttpClientConnectionManager connManager = newPoolingHttpClientConnectionManager(

        socketFactoryRegistry, connFactory, dnsResolver);


// Create socket configuration

SocketConfig socketConfig = SocketConfig.custom()

    .setTcpNoDelay(true)

    .build();

// Configure the connection manager to use socket configuration either

// by default or for a specific host.

connManager.setDefaultSocketConfig(socketConfig);

connManager.setSocketConfig(newHttpHost("somehost", 80), socketConfig);

// Validate connections after 1 sec of inactivity

connManager.setValidateAfterInactivity(1000);


// Create message constraints

MessageConstraints messageConstraints = MessageConstraints.custom()

    .setMaxHeaderCount(200)

    .setMaxLineLength(2000)

    .build();

// Create connection configuration

ConnectionConfig connectionConfig = ConnectionConfig.custom()

    .setMalformedInputAction(CodingErrorAction.IGNORE)

    .setUnmappableInputAction(CodingErrorAction.IGNORE)

    .setCharset(Consts.UTF_8)

    .setMessageConstraints(messageConstraints)

    .build();

// Configure the connection manager to use connection configuration either

// by default or for a specific host.

connManager.setDefaultConnectionConfig(connectionConfig);

connManager.setConnectionConfig(newHttpHost("somehost", 80), ConnectionConfig.DEFAULT);


// Configure total max or per route limits for persistent connections

// that can be kept in the pool or leased by the connection manager.

connManager.setMaxTotal(100);

connManager.setDefaultMaxPerRoute(10);

connManager.setMaxPerRoute(newHttpRoute(newHttpHost("somehost", 80)), 20);

完整示例请参考 ClientConfiguration.

基本上,在 Java 原生标准库不给力的情况下,Apache HttpComponents HttpClient 是最佳的 HTTP Client library 选择。但这个库当前还不支持 HTTP/2,支持 HTTP/2 的版本还处于 beta 阶段(2018.09.23),因此并不适合用于 Android APP 中使用。

OkHttp

由于当前 Apache HttpComponents HttpClient 版本并不支持 HTTP/2, 而 HTTP/2 对于移动客户端而言,无论是从握手延迟、响应延迟,还是资源开销看都有相当吸引力。因此这就给了高层次封装且支持 HTTP/2 的 http client lib 足够的生存空间。其中最典型的要数OkHttp.


importokhttp3.*;

importorg.apache.http.util.CharsetUtils;

importjava.util.HashMap;

importjava.util.Map;



publicclassOkHttpDemo {

    OkHttpClient client = newOkHttpClient();


    privateString sendPostForm(String url, finalMap<String, String> params) throwsException {

        FormBody.Builder builder = newFormBody.Builder(CharsetUtils.get("UTF-8"));

        if(params != null) {

            for(Map.Entry<String, String> entry: params.entrySet()) {

                builder.add(entry.getKey(), entry.getValue());

            }

        }

        RequestBody requestBody = builder.build();

        Request request = newRequest.Builder().url(url).post(requestBody).build();

        returnclient.newCall(request).execute().body().string();


    }


    privateString sendGet(String url) throwsException {

        Request request = newRequest.Builder().url(url).build();

        returnclient.newCall(request).execute().body().string();

    }


    publicstaticvoidmain(String[] args) throwsException {

        OkHttpDemo okHttpDemo = newOkHttpDemo();

        String url = "https://httpbin.org/post";

        Map<String, String> params = newHashMap<String, String>();

        params.put("foo", "bar中文");

        String rsp = okHttpDemo.sendPostForm(url, params);

        System.out.println("http post rsp:"+ rsp);


        url = "https://httpbin.org/get";

        System.out.println("http get rsp:"+ okHttpDemo.sendGet(url));

    }

}

OkHttp 接口设计友好,支持 HTTP/2,并且在弱网和无网环境下有自动检测和恢复机制,因此,是当前 Android APP 开发中使用最广泛的 HTTP clilent lib 之一。

另一方面,OkHttp 提供的接口与 Java 9 中 HttpClint 接口比较类似 (严格讲,应该是 Java 9 借鉴了 OkHttp 等开源库的接口设计?),因此,对于喜欢减少依赖,钟情于原生标准库的开发者来说,在 Java 11 中,从 OkHttp 切换到标准库是相对容易的。因此,以 OkHttp 为代表的 http 库以后的使用场景可能会被蚕食一部分。

小结

HttpURLConnection 封装层次太低,并且支持特性太少,不建议在项目中使用。除非你的确不想引入第三方 HTTP 依赖(如减少包大小、目标环境不提供三方库支持等)。

Java 9 中引入的 HttpClient,封装层次和支持特性都不错。但是因为 Java 版本的原因,应用场景还十分有限,建议观望一段时间再考虑在线上使用。

如果你不需要 HTTP/2特性,Apache HttpComponents HttpClient 是你的最佳选择,比如在服务器之间的 HTTP 调用。否则,请使用 OkHttp, 如 Android 开发。

扩展阅读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容