HttpClient入门及其应用 顶

1. 先来看几个需求

  • 项目中需要与一个基于HTTP协议的第三方的接口进行对接
  • 项目中需要动态的调用WebService服务(不生成本地源码)
  • 项目中需要利用其它网站的相关数据

这些需求可能或多或少的会发生在平时的开发中,针对每种情况,可能解决方案不止一种。本文将会使用HttpClient这种工具来讲解HttpClient的相关知识,以及如何使用HttpClient完成上述需求。

2. HttpClient是什么

HttpClient是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。(来源于百度百科)

——| 有人说,HttpClient不就是一个浏览器嘛。。。

可能不少人对HttpClient会产生这种误解,他们的观点是这样的:既然HttpClient是一个HTTP客户端编程工具,那不就相当于是一个浏览器了吗?无非它不能把HTML渲染出页面而已罢了。

其实HttpClient不是浏览器,它是一个HTTP通信库、一个工具包,因此它只提供一个通用浏览器应用程序所期望的功能子集。HttpClient与浏览器最根本的区别是:HttpClient中没有用户界面,浏览器需要一个渲染引擎来显示页面,并解释用户输入(例如鼠标点击显示页面上的某处之后如何响应、计算如何显示HTML页面、级联样式表和图像、javascript解释器运行嵌入HTML页面或从HTML页面引用的javascript代码、来自用户界面的事件被传递到javascript解释器进行处理等等等等)。HttpClient只能以编程的方式通过其API用于传输和接受HTTP消息,它对内容也是完全不可知的。

3. 为什么要用HttpClient,它跟同类产品有什么区别呢

提到HttpClient,就不得不提jdk原生的URL了。

jdk中自带了基本的网络编程,也就是java.net包下的一系列API。通过这些API,也可以完成网络编程和访问。

此外,另一个开源项目jsoup,它是一个简单的HTML解析器,可以直接解析指定URL请求地址的内容,它可以通过DOM方式来取数据,也是比较方便的API。

那既然已经有这些工具了,为什么还是有好多好多使用HttpClient的呢?

这里其实是有一个错误的认识:Jsoup是解析器不假,但它跟HttpClient不是同类产品(类似Hibernate和MyBatis),实际上日常使用通常会用HttpClient配合Jsoup做网页爬虫。

HttpClient还是有很多好的特点(摘自Apache HttpClient官网):

  • 基于标准、纯净的java语言。实现了HTTP1.0和HTTP1.1;
  • 以可扩展的面向对象的结构实现了HTTP全部的方法(GET, POST等7种方法);
  • 支持HTTPS协议;
  • 通过HTTP代理建立透明的连接;
  • 利用CONNECT方法通过HTTP代理建立隧道的HTTPS连接;
  • Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos认证方案;
  • 插件式的自定义认证方案;
  • 便携可靠的套接字工厂使它更容易的使用第三方解决方案;
  • 连接管理器支持多线程应用;支持设置最大连接数,同时支持设置每个主机的最大连接数,发现并关闭过期的连接;
  • 自动处理Set-Cookie中的Cookie;
  • 插件式的自定义Cookie策略;
  • Request的输出流可以避免流中内容直接缓冲到socket服务器;
  • Response的输入流可以有效的从socket服务器直接读取相应内容;
  • 在HTTP1.0和HTTP1.1中利用KeepAlive保持持久连接;
  • 直接获取服务器发送的response code和 headers;
  • 设置连接超时的能力;
  • 实验性的支持HTTP1.1 response caching;
  • 源代码基于Apache License 可免费获取。

4. HttpClient能干嘛

正如你所想,上面的需求全部都可以使用HttpClient完成。

HttpClient的功能包括但不限于:

  • 模拟浏览器发送HTTP请求,并接收响应
  • RPC接口调用
  • 爬取网页源码
  • 批量事务请求
  • …………

说的HttpClient那么好,它究竟怎么用呢?

5. HttpClient的实际使用

搭建Maven工程,需要导入HttpClient的相关jar包。

注意有两个HttpClient的工程,都导入,因为这是两个不同的项目,而我们在下面的用例中都会用到。

一个是单独的HttpClient,另一个是commons的HttpClient,不要搞混了哦!

注:下述没有标注commons的HttpClient都是通常讲的HttpClient,只有标注了commons-HttpClient,那才是工具包下的HttpClient哦(有点绕。。。)

(为了方便后续的几个需求,事先导入了Apache的commons相关工具包,jsoup解析器,和fastjson)

<dependencies>
     <dependency>
         <groupId>org.apache.httpcomponents</groupId>
         <artifactId>httpclient</artifactId>
         <version>4.5.6</version>
     </dependency>
     <dependency>
         <groupId>commons-httpclient</groupId>
         <artifactId>commons-httpclient</artifactId>
         <version>3.1</version>
     </dependency>
     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
         <version>3.7</version>
     </dependency>
     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-collections4</artifactId>
         <version>4.2</version>
     </dependency>
     <dependency>
         <groupId>commons-io</groupId>
         <artifactId>commons-io</artifactId>
         <version>2.6</version>
     </dependency>
     <dependency>
         <groupId>org.jsoup</groupId>
         <artifactId>jsoup</artifactId>
         <version>1.11.3</version>
     </dependency>
     <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.49</version>
     </dependency>
</dependencies>

至于具体的使用,我们来实现一下上面的三个需求吧!

6. HttpClient用例1:接口对接

6.1 基础Demo

我们使用淘宝网提供的手机归属地查询接口来进行接口对接:

https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=手机号

首先,我们很明显可以看出这是使用HTTP的get请求。

之后我们来编写源码进行接口对接。

public class RpcConsumer {
    public static void main(String[] args) throws Exception {
        //1. 创建HttpClient对象
        CloseableHttpClient client = HttpClients.createDefault();
        //2. 声明要请求的url,并构造HttpGet请求
        String url = "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13999999999";
        HttpGet get = new HttpGet(url);
        //3. 让HttpClient去发送get请求,得到响应
        CloseableHttpResponse response = client.execute(get);
        //4. 提取响应正文,并打印到控制台
        InputStream is = response.getEntity().getContent();
        String ret = IOUtils.toString(is, "GBK");
        System.out.println(ret);
    }
}

难度还是比较小的,但是我们在实际开发中绝对不能这么写,url和参数全被写死了,那你估计也要被打死了(滑稽)。接下来,我们来把这个调用者改为工具类。

6.2 通用抽取为工具类

首先,作为工具类,我们要动态接收url和参数,而不是在代码中写死。

构造RpcHttpUtil类,并从中封装invokeHttp方法如下:

public class RpcHttpUtil {
    public static final String GET = "GET";
    public static final String POST = "POST";
    
    public static Map<String, String> invokeHttp(String url, String method, 
            Map<String, String> paramMap, List<String> returnParamList) throws UnsupportedOperationException, IOException {
        //1. 创建HttpClient对象和响应对象
        CloseableHttpClient client = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        //2. 判断请求方法是get还是post
        if (StringUtils.equalsIgnoreCase(method, GET)) {
            //2.1 如果是get请求,要拼接请求url的参数
            StringBuilder urlSb = new StringBuilder(url);
            int paramIndex = 0;
            for (Entry<String, String> entry : paramMap.entrySet()) {
                //get请求要追加参数,中间有一个?
                if (paramIndex == 0) {
                    urlSb.append("?");
                }
                //拼接参数
                urlSb.append(entry.getKey() + "=" + entry.getValue() + "&");
            }
            //前面在拼接参数时最后多了一个&,应去掉
            urlSb.delete(urlSb.length() - 1, urlSb.length());
            HttpGet get = new HttpGet(urlSb.toString());
            //2.2 让HttpClient去发送get请求,得到响应
            response = client.execute(get);
        }else if (StringUtils.equalsIgnoreCase(method, POST)) {
            HttpPost post = new HttpPost(url);
            //2.3 如果是post请求,要构造虚拟表单,并封装参数
            List<NameValuePair> paramList = new ArrayList<>();
            for (Entry<String, String> entry : paramMap.entrySet()) {
                paramList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
            //2.4 设置请求正文的编码
            UrlEncodedFormEntity uefEntity = new UrlEncodedFormEntity(paramList, "GBK");
            post.setEntity(uefEntity);
            //2.5 让HttpClient去发送post请求,得到响应
            response = client.execute(post);
        }else {
            //其他请求类型不支持
            throw new RuntimeException("对不起,该请求方式不支持!");
        }
        //3. 提取响应正文,并封装成Map
        InputStream is = response.getEntity().getContent();
        Map<String, String> returnMap = new LinkedHashMap<>();
        String ret = IOUtils.toString(is, "GBK");
        //循环正则表达式匹配(因为有多个参数,无法预处理Pattern)
        for (String param : returnParamList) {
            Pattern pattern = Pattern.compile(param + ":['\"]?.+['\"]?");
            Matcher matcher = pattern.matcher(ret);
            while (matcher.find()) {
                String keyAndValue = matcher.group();
                String value = keyAndValue.substring(keyAndValue.indexOf("'") + 1, keyAndValue.lastIndexOf("'"));
                returnMap.put(param, value);
            }
            //如果没有匹配到,则put进空串(jdk8的方法)
            returnMap.putIfAbsent(param, "");
        }
        return returnMap;
    }
    
    private RpcHttpUtil() {
    }
}

之后测试方法:

public class RpcConsumer {
    public static void main(String[] args) throws Exception {
        //初始化参数列表和返回值取值列表
        Map<String, String> paramMap = new LinkedHashMap<String, String>() {{
            put("tel", "13999999999");
        }};
        List<String> returnParamList = new ArrayList<String>() {{
            add("province");
        }};
        //调用工具类
        Map<String, String> ret = RpcHttpUtil.invokeHttp(
                "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm", 
                RpcHttpUtil.GET, paramMap, returnParamList);
        System.out.println(ret);
    }
}

运行结果:{province=新疆}

7. HttpClient用例2:动态调用WebService服务(不生成本地源码)

使用commons-HttpClient,配合SOAP协议,可以实现不生成本地源码的前提下,也能调用WebService服务。

7.1 为什么使用commons-HttpClient可以成功调用WebService服务呢?

我们说,WebService是基于SOAP协议的,我们使用本地源码发送的请求,其实也就是这些基于SOAP的POST请求,收到的响应也是基于SOAP的响应。

那么,如果我们自己构造基于SOAP协议的POST请求,是不是服务也就可以正常返回结果呢?当然是肯定的!

不过,唯一不太好的是:自行构造源码,获得响应后需要自行解析响应体。

接下来我们要先了解SOAP的xml请求体格式,然后才能使用commons-HttpClient进行WebService的请求。

7.2 SOAP协议的请求体xml格式(精简)

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <[method] xmlns="[namaspace]">
            <[args]>[text]</[args]>
        </[method]>
    </soap:Body>
</soap:Envelope>

上面的格式中,方括号内的标识为具体WebService的请求。

举个简单的栗子吧:

url为http://ws.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl

里面的namespace要从wsdl中找:

之后构造请求xml(精简):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <qqCheckOnline xmlns="http://WebXml.com.cn/">
            <qqCode>10000</qqCode>
        </qqCheckOnline>
    </soap:Body>
</soap:Envelope>

7.3 使用commons-HttpClient发送POST请求,调用WebService服务

public class App {
    public static void main(String[] args) throws Exception {
        String url = " http://ws.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
        StringBuilder sb = new StringBuilder();
        sb.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">");
        sb.append("  <soap:Body>");
        sb.append("    <qqCheckOnline xmlns=\" http://WebXml.com.cn/\">");
        sb.append("      <qqCode>10000</qqCode>");
        sb.append("    </qqCheckOnline>");
        sb.append("  </soap:Body>");
        sb.append("</soap:Envelope>");
        PostMethod postMethod = new PostMethod(url);
        byte[] bytes = sb.toString().getBytes("utf-8");
        InputStream inputStream = new ByteArrayInputStream(bytes, 0, bytes.length);
        RequestEntity requestEntity = new InputStreamRequestEntity(inputStream, bytes.length, "text/xml;charset=UTF-8");
        postMethod.setRequestEntity(requestEntity);
        
        HttpClient httpClient = new HttpClient();
        httpClient.executeMethod(postMethod);
        String soapResponseData = postMethod.getResponseBodyAsString();
        System.out.println(soapResponseData);
    }
}

请求结果(响应体真的没有换行符号,直接一行出来了。。。):

<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><qqCheckOnlineResponse xmlns="http://WebXml.com.cn/"><qqCheckOnlineResult>N</qqCheckOnlineResult></qqCheckOnlineResponse></soap:Body></soap:Envelope>

7.4 提取响应数据

我们完全可以使用Dom4j来提取响应体的数据,但是Dom4j只能一层一层的扒,太费劲。Jsoup不仅仅可以解析HTML文档,也可以进行xml转换和提取。

之后向刚才的源码追加如下内容,便可以只输出想要的返回结果。

Document document = Jsoup.parse(soapResponseData);
String text = document.getElementsByTag("qqCheckOnlineResult").text();
System.out.println(text);
//输出结果:N

更详细的WebService资料,请移步:https://my.oschina.net/LinkedBear/blog/1928400

8. HttpClient用例3:网络爬虫

前边我们说过,HttpClient配合Jsoup可以完成网络爬虫的任务,接下来我们来实际做一个爬虫:爬取京东商城-笔记本电脑的商品信息。

8.1 爬取列表页

京东商城-笔记本电脑-商品列表页url:https://list.jd.com/list.html?cat=670,671,672

8.1.1 网页分析

我们要爬取的位置在这里:

所有的商品,构成一个ul,每一个商品都是一个li:

可以看出,一个a标签中嵌套进了这个商品的图片,我们只需要提取这个a标签的链接即可。

8.1.2 HttpClient+Jsoup爬取列表页

编写爬虫程序如下:

public class Crawler {
    public static void main(String[] args) throws Exception {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://list.jd.com/list.html?cat=670,671,672");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
        Document document = Jsoup.parse(html);
        Elements goodsDivs = document.getElementsByClass("j-sku-item");
        for (Element goodsDiv : goodsDivs) {
            String href = "https:" + goodsDiv.getElementsByClass("p-img").get(0)
                    .getElementsByTag("a").get(0).attr("href");
            System.out.println(href);
        }
    }
}

可爬取商品链接如下:

之后遍历这些连接,依次进入:

本次我们不做太难的数据处理,只爬取商品名、商品价格以及商品的基本参数。

8.2 爬取单个商品的信息

8.2.1 网页分析

打开https://item.jd.com/7418428.html,可以提取到相关数据如下:

8.2.2 HttpClient+Jsoup爬取商品信息+详情

编写爬虫程序如下:

public class Crawler2 {
    public static void main(String[] args) throws Exception {
        String goodsId = "7418428";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://item.jd.com/" + goodsId + ".html");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "GBK");
        Document document = Jsoup.parse(html);
        String goodsName = document.getElementsByClass("sku-name").get(0).text();
        System.out.println(goodsName);
        String goodsPrice = document.getElementsByClass("price J-p-" + goodsId).get(0).text();
        System.out.println(goodsPrice);
        Element paramList = document.getElementsByClass("p-parameter").get(0)
                .getElementsByClass("parameter2").get(0);
        Elements params = paramList.getElementsByTag("li");
        for (Element param : params) {
            System.out.println(param.attr("title") + " - " + param.text());
        }
    }
}

爬取结果:

价格没有拿到!说明价格不在我们当前的页面请求上,而是ajax请求获取到的!

需要再用HttpClient请求一次获取价格的链接,才可以正常获取商品价格。

加入修改后的商品价格请求的爬虫源码如下:

public class Crawler2 {
    public static void main(String[] args) throws Exception {
        String goodsId = "7418428";
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet("https://item.jd.com/" + goodsId + ".html");
        CloseableHttpResponse response = client.execute(get);
        String html = IOUtils.toString(response.getEntity().getContent(), "GBK");
        Document document = Jsoup.parse(html);
        //取商品名
        String goodsName = document.getElementsByClass("sku-name").get(0).text();
        System.out.println(goodsName);
        //取商品价格
        //String goodsPrice = document.getElementsByClass("price J-p-" + goodsId).get(0).text();
        //System.out.println(goodsPrice);
        //价格属于ajax请求,需要单独发送一个请求,获取价格(此链接返回json数组且长度为1)
        String priceUrl = "http://p.3.cn/prices/get?type=1&skuid=J_" + goodsId;
        HttpPost post = new HttpPost(priceUrl);
        CloseableHttpResponse priceResponse = client.execute(post);
        String jsonStr = IOUtils.toString(priceResponse.getEntity().getContent(), "UTF-8");
        JSONObject json = JSONArray.parseArray(jsonStr).getJSONObject(0);
        System.out.println(json.getString("p"));
        //加载商品详情
        Element paramList = document.getElementsByClass("p-parameter").get(0)
                .getElementsByClass("parameter2").get(0);
        Elements params = paramList.getElementsByTag("li");
        for (Element param : params) {
            System.out.println(param.attr("title") + " - " + param.text());
        }
    }
}

运行,可以正常获取结果。

(完)

本文相关源码可从码云获取:

https://gitee.com/linkedbear/HttpClient-Demo​​​​​​​

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏圣杰的专栏

.Net异步编程知多少

1. 引言 最近在学习Abp框架,发现Abp框架的很多Api都提供了同步异步两种写法。异步编程说起来,大家可能都会说异步编程性能好。但好在哪里,引入了什么问题,...

2197
来自专栏跟着阿笨一起玩NET

WebService基于SoapHeader实现安全认证

      本文仅提供通过设置SoapHeader来控制非法用户对WebService的调用,如果是WebService建议使用WSE3.0来保护Web服务,如...

1382
来自专栏开发 & 算法杂谈

Hiredis源码阅读(二)

上一篇介绍了Hiredis中的同步api以及回复解析api,这里紧接着介绍异步api。异步api需要与事件库(libevent、libev、ae一起工作)。

3705
来自专栏有趣的django

Django rest framework(7)----分页

第一种分页  PageNumberPagination 基本使用 (1)urls.py urlpatterns = [ re_path('(?P<ve...

5967
来自专栏AhDung

C#程序防多开又一法

在Main()方法开始时遍历所有进程,获取每个进程的程序集GUID和PID,若发现有跟自己GUID相同且PID不同的进程,就勒令自身退出。

2003
来自专栏大内老A

WCF版的PetShop之三:实现分布式的Membership和上下文传递

通过上一篇了解了模块内基本的层次划分之后,接下来我们来聊聊PetShop中一些基本基础功能的实现,以及一些设计、架构上的应用如何同WCF进行集成。本篇讨论两个问...

2565
来自专栏pangguoming

C# 简单日志文本输出

第一种  直接文件IO流写日志文件 using System.IO; public static void WriteLog(string strLog) { ...

3245
来自专栏技术博客

菜菜从零学习WCF九(会话、实例化和并发)

在服务协定上设置System.ServiceModel.ServiceContractAttribute.SessionMode值

883
来自专栏码农阿宇

ASP.NET Core轻松入门Bind读取配置文件到C#实例

首先新建一个ASP.NET Core空项目,命名为BindReader ? 然后 向项目中添加一个名为appsettings.json的json文件,为什么叫a...

5185
来自专栏大内老A

在ASP.NET MVC中通过URL路由实现对多语言的支持

对于一个需要支持多语言的Web应用,一个很常见的使用方式就是通过请求地址来控制界面呈现所基于的语言文化,比如我们在表示请求地址的URL中将上语言文化代码(比如e...

2126

扫码关注云+社区