https://www.hoshino.asia/archives/hutool

概述

由来

在Java的世界中,Http客户端之前一直是Apache家的HttpClient占据主导,但是由于此包较为庞大,API又比较难用,因此并不适用很多场景。而新兴的OkHttp、Jodd-http固然好用,但是面对一些场景时,学习成本还是有一些的。很多时候,我们想追求轻量级的Http客户端,并且追求简单易用。而JDK自带的HttpUrlConnection可以满足大部分需求。Hutool针对此类做了一层封装,使Http请求变得无比简单。

介绍

Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS请求、文件上传、Cookie记忆等操作,使Http请求变得无比简单。

Hutool-http的核心集中在两个类:

  • HttpRequest

  • HttpResponse

同时针对大部分情境,封装了HttpUtil工具类。

Hutool-http优点

  1. 根据URL自动判断是请求HTTP还是HTTPS,不需要单独写多余的代码。

  2. 表单数据中有File对象时自动转为multipart/form-data表单,不必单独做操作。

  3. 默认情况下Cookie自动记录,比如可以实现模拟登录,即第一次访问登录URL后后续请求就是登录状态。

  4. 自动识别304跳转并二次请求

  5. 自动识别页面编码,即根据header信息或者页面中的相关标签信息自动识别编码,最大可能避免乱码。

  6. 自动识别并解压Gzip格式返回内容

使用

最简单的使用莫过于用HttpUtil工具类快速请求某个页面:

//GET请求
String content = HttpUtil.get(url);

一行代码即可搞定,当然Post请求也很简单:

//POST请求
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("city", "北京");

String result1 = HttpUtil.post(url, paramMap);

Post请求只需使用Map预先制定form表单项即可。

更多

根据Hutool的“便捷性与灵活性并存”原则,HttpUtil的存在体现了便捷性,那HttpRequest对象的使用则体现了灵活性,使用此对象可以自定义更多的属性给请求,以适应Http请求中的不同场景(例如自定义header、自定义cookie、自定义代理等等)。相关类的使用请见下几个章节。

Http客户端工具类-HttpUtil

概述

HttpUtil是应对简单场景下Http请求的工具类封装,此工具封装了HttpRequest对象常用操作,可以保证在一个方法之内完成Http请求。

此模块基于JDK的HttpUrlConnection封装完成,完整支持https、代理和文件上传。

使用

请求普通页面

针对最为常用的GET和POST请求,HttpUtil封装了两个方法,

  • HttpUtil.get

  • HttpUtil.post

这两个方法用于请求普通页面,然后返回页面内容的字符串,同时提供一些重载方法用于指定请求参数(指定参数支持File对象,可实现文件上传,当然仅仅针对POST请求)。

GET请求栗子:

// 最简单的HTTP请求,可以自动通过header等信息判断编码,不区分HTTP和HTTPS
String result1= HttpUtil.get("https://www.baidu.com");

// 当无法识别页面编码的时候,可以自定义请求页面的编码
String result2= HttpUtil.get("https://www.baidu.com", CharsetUtil.CHARSET_UTF_8);

//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("city", "北京");

String result3= HttpUtil.get("https://www.baidu.com", paramMap);

POST请求例子:

HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("city", "北京");

String result= HttpUtil.post("https://www.baidu.com", paramMap);

文件上传

HashMap<String, Object> paramMap = new HashMap<>();
//文件上传只需将参数中的键指定(默认file),值设为文件对象即可,对于使用者来说,文件上传与普通表单提交并无区别
paramMap.put("file", FileUtil.file("D:\\face.jpg"));

String result= HttpUtil.post("https://www.baidu.com", paramMap);

下载文件

因为Hutool-http机制问题,请求页面返回结果是一次性解析为byte[]的,如果请求URL返回结果太大(比如文件下载),那内存会爆掉,因此针对文件下载HttpUtil单独做了封装。文件下载在面对大文件时采用流的方式读写,内存中只是保留一定量的缓存,然后分块写入硬盘,因此大文件情况下不会对内存有压力。

String fileUrl = "http://mirrors.sohu.com/centos/8.4.2105/isos/x86_64/CentOS-8.4.2105-x86_64-dvd1.iso";

//将文件下载后保存在E盘,返回结果为下载文件大小
long size = HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"));
System.out.println("Download size: " + size);

当然,如果我们想感知下载进度,还可以使用另一个重载方法回调感知下载进度:

//带进度显示的文件下载
HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"), new StreamProgress(){
	
	@Override
	public void start() {
		Console.log("开始下载。。。。");
	}
	
	@Override
	public void progress(long progressSize) {
		Console.log("已下载:{}", FileUtil.readableFileSize(progressSize));
	}
	
	@Override
	public void finish() {
		Console.log("下载完成!");
	}
});

StreamProgress接口实现后可以感知下载过程中的各个阶段。

当然,工具类提供了一个更加抽象的方法:HttpUtil.download,此方法会请求URL,将返回内容写入到指定的OutputStream中。使用这个方法,可以更加灵活的将HTTP内容转换写出,以适应更多场景。

更多有用的工具方法

  • HttpUtil.encodeParams 对URL参数做编码,只编码键和值,提供的值可以是url附带参数,但是不能只是url

  • HttpUtil.toParamsHttpUtil.decodeParams 两个方法是将Map参数转为URL参数字符串和将URL参数字符串转为Map对象

  • HttpUtil.urlWithForm是将URL字符串和Map参数拼接为GET请求所用的完整字符串使用

  • HttpUtil.getMimeType 根据文件扩展名快速获取其MimeType(参数也可以是完整文件路径)

更多请求参数

如果想设置头信息、超时、代理等信息,请见章节《Http客户端-HttpRequest》。

HTML工具类-HtmlUtil

由来

针对Http请求中返回的Http内容,Hutool使用此工具类来处理一些HTML页面相关的事情。

比如我们在使用爬虫爬取HTML页面后,需要对返回页面的HTML内容做一定处理,比如去掉指定标签(例如广告栏等)、去除JS、去掉样式等等,这些操作都可以使用HtmlUtil完成。

方法

HtmlUtil.escape

转义HTML特殊字符,包括:

  1. ' 替换为 &#039;

  2. " 替换为 &quot;

  3. & 替换为 &amp;

  4. < 替换为 &lt;

  5. > 替换为 &gt;

String html = "<html><body>123'123'</body></html>";
// 结果为:&lt;html&gt;&lt;body&gt;123&#039;123&#039;&lt;/body&gt;&lt;/html&gt;
String escape = HtmlUtil.escape(html);

HtmlUtil.unescape

还原被转义的HTML特殊字符

String escape = "&lt;html&gt;&lt;body&gt;123&#039;123&#039;&lt;/body&gt;&lt;/html&gt;";
// 结果为:<html><body>123'123'</body></html>
String unescape = HtmlUtil.unescape(escape);

HtmlUtil.removeHtmlTag

清除指定HTML标签和被标签包围的内容

String str = "pre<img src=\"xxx/dfdsfds/test.jpg\">";
// 结果为:pre
String result = HtmlUtil.removeHtmlTag(str, "img");

HtmlUtil.cleanHtmlTag

清除所有HTML标签,但是保留标签内的内容

String str = "pre<div class=\"test_div\">\r\n\t\tdfdsfdsfdsf\r\n</div><div class=\"test_div\">BBBB</div>";
// 结果为:pre\r\n\t\tdfdsfdsfdsf\r\nBBBB
String result = HtmlUtil.cleanHtmlTag(str);

HtmlUtil.unwrapHtmlTag

清除指定HTML标签,不包括内容

String str = "pre<div class=\"test_div\">abc</div>";
// 结果为:preabc
String result = HtmlUtil.unwrapHtmlTag(str, "div");

HtmlUtil.removeHtmlAttr

去除HTML标签中的指定属性,如果多个标签有相同属性,都去除

String html = "<div class=\"test_div\"></div><span class=\"test_div\"></span>";
// 结果为:<div></div><span></span>
String result = HtmlUtil.removeHtmlAttr(html, "class");

HtmlUtil.removeAllHtmlAttr

去除指定标签的所有属性

String html = "<div class=\"test_div\" width=\"120\"></div>";
// 结果为:<div></div>
String result = HtmlUtil.removeAllHtmlAttr(html, "div");

HtmlUtil.filter 过滤HTML文本,防止XSS攻击

String html = "<alert></alert>";
// 结果为:""
String filter = HtmlUtil.filter(html);

Http响应-HttpResponse

介绍

HttpResponse是HttpRequest执行execute()方法后返回的一个对象,我们可以通过此对象获取服务端返回的:

  • Http状态码(getStatus方法)

  • 返回内容编码(contentEncoding方法)

  • 是否Gzip内容(isGzip方法)

  • 返回内容(body、bodyBytes、bodyStream方法)

  • 响应头信息(header方法)

使用

此对象的使用非常简单,最常用的便是body方法,会返回Http响应内容字符串。如果想获取byte[]则调用bodyBytes即可。

获取响应状态码

HttpResponse res = HttpRequest.post(url).execute();
Console.log(res.getStatus());

获取响应头信息

HttpResponse res = HttpRequest.post(url).execute();
//预定义的头信息
Console.log(res.header(Header.CONTENT_ENCODING));
//自定义头信息
Console.log(res.header("Content-Disposition"));

Http请求-HttpRequest

介绍

本质上,HttpUtil中的get和post工具方法都是HttpRequest对象的封装,因此如果想更加灵活操作Http请求,可以使用HttpRequest。

使用

普通表单

我们以POST请求为例:

//链式构建请求
String result2 = HttpRequest.post(url)
	.header(Header.USER_AGENT, "Hutool http")//头信息,多个头信息多次调用此方法即可
	.form(paramMap)//表单内容
	.timeout(20000)//超时,毫秒
	.execute().body();
Console.log(result2);

通过链式构建请求,我们可以很方便的指定Http头信息和表单信息,最后调用execute方法即可执行请求,返回HttpResponse对象。HttpResponse包含了服务器响应的一些信息,包括响应的内容和响应的头信息。通过调用body方法即可获取响应内容。

Restful请求

String json = ...;
String result2 = HttpRequest.post(url)
	.body(json)
	.execute().body();

配置代理

如果代理无需账号密码,可以直接:

String result2 = HttpRequest.post(url)
	.setHttpProxy("127.0.0.1", 9080)
	.body(json)
	.execute().body();

如果需要自定其他类型代理或更多的项目,可以:

String result2 = HttpRequest.post(url)
	.setProxy(new Proxy(Proxy.Type.HTTP,
				new InetSocketAddress(host, port))
	.body(json)
	.execute().body();

如果遇到https代理错误Proxy returns "HTTP/1.0 407 Proxy Authentication Required",可以尝试:

System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
Authenticator.setDefault(
    new Authenticator() {
        @Override
        public PasswordAuthentication getPasswordAuthentication() {
              return new PasswordAuthentication(authUser, authPassword.toCharArray());
        }
    }
);

其它自定义项

同样,我们通过HttpRequest可以很方便的做以下操作:

  • 指定请求头

  • 自定义Cookie(cookie方法)

  • 指定是否keepAlive(keepAlive方法)

  • 指定表单内容(form方法)

  • 指定请求内容,比如rest请求指定JSON请求体(body方法)

  • 超时设置(timeout方法)

  • 指定代理(setProxy方法)

  • 指定SSL协议(setSSLProtocol)

  • 简单验证(basicAuth方法)

UA工具类-UserAgentUtil

由来

User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、浏览器及版本、浏览器渲染引擎等。

Hutool在4.2.1之后支持User-Agent的解析。

使用

以桌面浏览器为例,假设你已经获取了用户的UA:

String uaStr = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1";

获取UA信息

我们可以借助UserAgentUtil.parse方法解析:

UserAgent ua = UserAgentUtil.parse(uaStr);

ua.getBrowser().toString();//Chrome
ua.getVersion();//14.0.835.163
ua.getEngine().toString();//Webkit
ua.getEngineVersion();//535.1
ua.getOs().toString();//Windows 7
ua.getPlatform().toString();//Windows

判断终端是否为移动终端

ua.isMobile();

常用Http状态码-HttpStatus

介绍

针对Http响应,Hutool封装了一个类用于保存Http状态码

此类用于保存一些状态码的别名,例如:

/**
* HTTP Status-Code 200: OK.
*/
public static final int HTTP_OK = 200;

案例1-爬取开源中国的开源资讯

介绍

为了演示Hutool-http的http请求功能,因此这个栗子用红薯家的开源资讯开刀,在此做个简单的Demo。

开始

分析页面

  1. 打开红薯家的主页,我们找到最显眼的开源资讯模块,然后点击“更多”,打开“开源资讯”板块。

  1. 打开F12调试器,点击快捷键F12打开Chrome的调试器,点击“Network”选项卡,然后在页面上点击“全部资讯”。

  1. 由于红薯家的列表页是通过下拉翻页的,因此下拉到底部会触发第二页的加载,此时我们下拉到底部,然后观察调试器中是否有新的请求出现。如图,我们发现第二个请求是列表页的第二页。

  1. 我们打开这个请求地址,可以看到纯纯的内容。红框所指地址为第二页的内容,很明显p参数代表了页码page。

  1. 我们右键点击后查看源码,可以看到源码。

  1. 找到标题部分的HTML源码,然后搜索这个包围标题的HTML部分,看是否可以定位标题。

至此分析完毕,我们拿到了列表页的地址,也拿到了可以定位标题的相关字符(在后面用正则提取标题用),就可以开始使用Hutool编码了。

模拟Http请求爬取页面

使用Hutool-http配合ReUtil请求并提取页面内容非常简单,代码如下:

//请求列表页
String listContent = HttpUtil.get("https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=2");
//使用正则获取所有标题
List<String> titles = ReUtil.findAll("<span class=\"text-ellipsis\">(.*?)</span>", listContent, 1);
for (String title : titles) {
	//打印标题
	Console.log(title);
}

抓取结果为:

其实核心就前两行代码,第一行请求页面内容,第二行正则定位所有标题行并提取标题部分。

这里我解释下正则部分:ReUtil.findAll方法用于查找所有匹配正则表达式的内容部分,第二个参数1表示提取第一个括号(分组)中的内容,0表示提取所有正则匹配到的内容。这个方法可以看下core模块中ReUtil章节了解详情。

<span class=\"text-ellipsis\">(.*?)</span>这个正则就是我们上面分析页面源码后得到的正则,其中(.*?)表示我们需要的内容,.表示任意字符,*表示0个或多个,?表示最短匹配,整个正则的意思就是。,以<span class=\"text-ellipsis\">开头,</span>结尾的中间所有字符,中间的字符要达到最短。?的作用其实就是将范围限制到最小,不然</span>很可能匹配到后面去了。

关于正则表达式这块可以看下我的博客:正则表达式简明参考

结语

不得不说,抓取本身并不困难,尤其配合Hutool会让这项工作变得更加简单快速,而其中的难点便是分析页面和定位我们需要的内容。

真正的内容抓取分为四个部分:

  • 找到列表页(很多网站都没有一个总的列表页)

  • 请求列表页,获取详情页地址

  • 请求详情页并使用正则匹配我们需要的内容

  • 入库或将内容保存为文件

而且在抓取过程中我们也会遇到各种问题,包括但不限于:

  • 封IP

  • 对请求Header有特殊要求

  • 对Cookie有特殊要求

  • 验证码

这些问题都有一些解决办法,具体要在具体的开发中分析解决。

希望大家看到这个栗子有所启发,也为Hutool提供更多更好的意见~

常见问题

Received fatal alert: handshake_failure 错误

场景为使用Hutool-http请求https服务器,原因是JDK中的JCE安全机制导致的问题解决方法如下:

  • 方法1:如果你使用的是JDK8,请升级到JDK8的最新版本(例如jdk1.8.0_181)。

  • 方法2:尝试添加以下代码:

System.setProperty("https.protocols", "TLSv1.2,TLSv1.1,SSLv3");

Server

简易Http服务器-SimpleServer

由来

Oracle JDK提供了一个简单的Http服务端类,叫做HttpServer,当然它是sun的私有包,位于com.sun.net.httpserver下,必须引入rt.jar才能使用,Hutool基于此封装了SimpleServer,用于在不引入Tomcat、Jetty等容器的情况下,实现简单的Http请求处理。

SimpleServer在Hutool-5.3.0后才引入,请升级到最新版本

使用

  1. 启动一个Http服务非常简单:

HttpUtil.createServer(8888).start();

通过浏览器访问 http://localhost:8888/ 即可,当然此时访问任何path都是404。

  1. 处理简单请求:

HttpUtil.createServer(8888)
    .addAction("/", (req, res)->{
        res.write("Hello Hutool Server");
    })
    .start();

此处我们定义了一个简单的action,绑定在"/"路径下,此时我们可以访问,输出“Hello Hutool Server”。

同理,我们通过调用addAction方法,定义不同path的处理规则,实现相应的功能。

简单的文件服务器

Hutool默认提供了简单的文件服务,即定义一个root目录,则请求路径后直接访问目录下的资源,默认请求index.html,类似于Nginx。

HttpUtil.createServer(8888)
    // 设置默认根目录
    .setRoot("D:\\workspace\\site\\hutool-site")
    .start();

此时访问http://localhost:8888/ 即可访问HTML静态页面。

hutool-site是Hutool主页的源码项目,地址在:https://gitee.com/loolly_admin/hutool-site,下载后配合SimpleServer实现离线文档。

读取请求和返回内容

有时候我们需要自定义读取请求参数,然后根据参数访问不同的数据,整理返回,此时我们自定义Action即可完成:

  1. 返回JSON数据

HttpUtil.createServer(8888)
    // 返回JSON数据测试
    .addAction("/restTest", (request, response) ->
    		response.write("{\"id\": 1, \"msg\": \"OK\"}", ContentType.JSON.toString())
    ).start();
  1. 获取表单数据并返回

HttpUtil.createServer(8888)
    // http://localhost:8888/formTest?a=1&a=2&b=3
    .addAction("/formTest", (request, response) ->
        response.write(request.getParams().toString(), ContentType.TEXT_PLAIN.toString())
    ).start();

文件上传

除了常规Http服务,Hutool还封装了文件上传操作:

HttpUtil.createServer(8888)
    .addAction("/file", (request, response) -> {
        final UploadFile file = request.getMultipart().getFile("file");
        // 传入目录,默认读取HTTP头中的文件名然后创建文件
        file.write("d:/test/");
        response.write("OK!", ContentType.TEXT_PLAIN.toString());
        }
    )
    .start();

WebService

Soap客户端-SoapClient

由来

在接口对接当中,WebService接口占有着很大份额,而我们为了使用这些接口,不得不引入类似Axis等库来实现接口请求。

现在有了Hutool,就可以在无任何依赖的情况下,实现简便的WebService请求。

使用

  1. 使用SoapUI解析WSDL地址,找到WebService方法和参数。

我们得到的XML模板为:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://WebXml.com.cn/">
   <soapenv:Header/>
   <soapenv:Body>
      <web:getCountryCityByIp>
         <!--Optional:-->
         <web:theIpAddress>?</web:theIpAddress>
      </web:getCountryCityByIp>
   </soapenv:Body>
</soapenv:Envelope>
  1. 按照SoapUI中的相应内容构建SOAP请求。

我们知道:

  1. 方法名为:web:getCountryCityByIp

  2. 参数只有一个,为:web:theIpAddress

  3. 定义了一个命名空间,前缀为web,URI为http://WebXml.com.cn/

这样我们就能构建相应SOAP请求:

// 新建客户端
SoapClient client = SoapClient.create("http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx")
    // 设置要请求的方法,此接口方法前缀为web,传入对应的命名空间
    .setMethod("web:getCountryCityByIp", "http://WebXml.com.cn/")
    // 设置参数,此处自动添加方法的前缀:web
    .setParam("theIpAddress", "218.21.240.106");

    // 发送请求,参数true表示返回一个格式化后的XML内容
    // 返回内容为XML字符串,可以配合XmlUtil解析这个响应
    Console.log(client.send(true));

扩展

查看生成的请求XML

调用SoapClient对象的getMsgStr方法可以查看生成的XML,以检查是否与SoapUI生成的一致。

SoapClient client = ...;
Console.log(client.getMsgStr(true));

多参数或复杂参数

对于请求体是列表参数或多参数的情况,如:

<web:method>
  <arg0>
    <fd1>aaa</fd1>
    <fd2>bbb</fd2>
  </arg0>
</web:method>

这类请求可以借助addChildElement完成。

SoapClient client = SoapClient.create("https://hutool.cn/WebServices/test.asmx")
		.setMethod("web:method", "http://hutool.cn/")
		SOAPElement arg0 = client.getMethodEle().addChildElement("arg0");
		arg0.addChildElement("fdSource").setValue("?");
		arg0.addChildElement("fdTemplated").setValue("?");

详细的问题解答见:https://gitee.com/dromara/hutool/issues/I4QL1V

Ciallo~(∠・ω< )⌒☆