概述
由来
在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优点
根据URL自动判断是请求HTTP还是HTTPS,不需要单独写多余的代码。
表单数据中有File对象时自动转为
multipart/form-data
表单,不必单独做操作。默认情况下Cookie自动记录,比如可以实现模拟登录,即第一次访问登录URL后后续请求就是登录状态。
自动识别304跳转并二次请求
自动识别页面编码,即根据header信息或者页面中的相关标签信息自动识别编码,最大可能避免乱码。
自动识别并解压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附带参数,但是不能只是urlHttpUtil.toParams
和HttpUtil.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特殊字符,包括:
'
替换为'
"
替换为"
&
替换为&
<
替换为<
>
替换为>
String html = "<html><body>123'123'</body></html>";
// 结果为:<html><body>123'123'</body></html>
String escape = HtmlUtil.escape(html);
HtmlUtil.unescape
还原被转义的HTML特殊字符
String escape = "<html><body>123'123'</body></html>";
// 结果为:<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。
开始
分析页面
打开红薯家的主页,我们找到最显眼的开源资讯模块,然后点击“更多”,打开“开源资讯”板块。
打开F12调试器,点击快捷键F12打开Chrome的调试器,点击“Network”选项卡,然后在页面上点击“全部资讯”。
由于红薯家的列表页是通过下拉翻页的,因此下拉到底部会触发第二页的加载,此时我们下拉到底部,然后观察调试器中是否有新的请求出现。如图,我们发现第二个请求是列表页的第二页。
我们打开这个请求地址,可以看到纯纯的内容。红框所指地址为第二页的内容,很明显p参数代表了页码page。
我们右键点击后查看源码,可以看到源码。
找到标题部分的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后才引入,请升级到最新版本
使用
启动一个Http服务非常简单:
HttpUtil.createServer(8888).start();
通过浏览器访问 http://localhost:8888/ 即可,当然此时访问任何path都是404。
处理简单请求:
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即可完成:
返回JSON数据
HttpUtil.createServer(8888)
// 返回JSON数据测试
.addAction("/restTest", (request, response) ->
response.write("{\"id\": 1, \"msg\": \"OK\"}", ContentType.JSON.toString())
).start();
获取表单数据并返回
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请求。
使用
使用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>
按照SoapUI中的相应内容构建SOAP请求。
我们知道:
方法名为:
web:getCountryCityByIp
参数只有一个,为:
web:theIpAddress
定义了一个命名空间,前缀为
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