avatar


14.HttpClient

关于Python中的"HttpClient",可以参考《爬虫及其Python实现:1.发送请求获取响应》

简介

在Java中,HttpClient,一般指的指Apache的那一款HttpClient
官网:http://hc.apache.org

迄今为止,HttpClient有三个大版本:
三种的HttpClient

  • commons-httpclient是3版本及之前的。
  • org.apache.httpcomponents是4版本的。
  • org.apache.httpcomponents.client5是5版本的,

在本章,我们以使用最多的4版本为例。

原生方式

除了HttpClient,JDK本身当然也支持发HTTP请求。我们简单的举一个JDK的原生方式。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.kakawanyifan;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;

public class Raw {
public static void main(String[] args) throws IOException {
// 需要请求的地址
String urlString = "https://kakawanyifan.com";
// 把字符串转成URL
URL url = new URL(urlString);
// 通过URL对象打开连接
URLConnection urlConnection = url.openConnection();
// 强制转换为HttpURLConnection
HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
// 获取输入流
InputStream inputStream = httpURLConnection.getInputStream();

InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null){
System.out.println(line);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Kaka Wan Yifan</title><meta name="description" content=""><meta name="author" content="Kaka Wan Yifan,i@m.kakawanyifan.com"><meta name="copyright" content="Kaka Wan Yifan"><meta name="format-detection" content="telephone=no"><link rel="shortcut icon" href="/img/favicon.ico"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="https://fonts.googleapis.com" crossorigin="crossorigin"/><meta name="google-site-verification" content="SzZHj5G5vHwv9JUmJD-bxmThc7a6YoZAsaNhwcD-BmM"/><meta name="msvalidate.01" content="1DD984606A0A3BC45A692A32685321AB"/><meta name="baidu-site-verification" content="99Pgg3yv8a"/><meta name="360-site-verification" content="273e85da5f4d732881979af78473b941"/><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Kaka Wan Yifan"><meta name="twitter:description" content=""><meta name="twitter:image" content="https://kakawanyifan.com/img/avatar.jpg"><meta property="og:type" content="website"><meta property="og:title" content="Kaka Wan Yifan"><meta property="og:url" content="https://kakawanyifan.com/"><meta property="og:site_name" content="Kaka Wan Yifan"><meta property="og:description" content=""><meta property="og:image" content="https://kakawanyifan.com/img/avatar.jpg"><script src="//unpkg.com/js-cookie/dist/js.cookie.min.js"></script><script>var autoChangeMode = 'false'
var t = Cookies.get("theme")
if (autoChangeMode == '1'){
var isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
var isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches

【部分运行结果略】

mermaid.initialize({
theme: 'default',
})
})
}</script></body></html>

JDK原生的方式,也支持做更多设置,例如:

  • 代理:url.openConnection(【代理】);
  • 请求方法:httpURLConnection.setRequestMethod();
  • 请求头:httpURLConnection.setRequestProperty();
  • 超时时间:
    • httpURLConnection.setConnectTimeout();
    • httpURLConnection.setReadTimeout();

Get

引入JAR包

首先,我们需要引入HttpClient的JAR包,POM坐标如下:

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

无参

我们以最简单的无参的Get请求为例,列举:

  • 发请求的过程
  • 获取响应状态码
  • 获取响应头
  • 获取响应体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.kakawanyifan;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class Get {
public static void main(String[] args) throws IOException {
// 创建http客户端
CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
// 创建get对象
String urlString = "https://kakawanyifan.com";
HttpGet httpGet = new HttpGet(urlString);
// 执行请求,获取响应体
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);

// 获取状态码
StatusLine statusLine = closeableHttpResponse.getStatusLine();
System.out.println("状态码:" + statusLine.getStatusCode());

// 获取响应头
Header[] allHeaders = closeableHttpResponse.getAllHeaders();
for (Header header: allHeaders) {
System.out.println("响应头:" + header.getName() + ":" + header.getValue());
}

// 获取响应体
HttpEntity entity = closeableHttpResponse.getEntity();
System.out.println("响应体:");
System.out.println(EntityUtils.toString(entity, StandardCharsets.UTF_8));
// 确保关闭
EntityUtils.consume(entity);

if (null != closeableHttpResponse){
closeableHttpResponse.close();
}

if (null != closeableHttpClient){
closeableHttpClient.close();
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
状态码:200
响应头:X-Powered-By:Hexo
响应头:Content-Type:text/html
响应头:Date:Tue, 22 Nov 2022 00:28:11 GMT
响应头:Connection:keep-alive
响应头:Keep-Alive:timeout=5
响应头:Transfer-Encoding:chunked
响应体:
<!DOCTYPE html><html lang="zh-CN" data-theme="light"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Kaka Wan Yifan</title><meta name="description" content=""><meta name="author" content="Kaka Wan Yifan,i@m.kakawanyifan.com"><meta name="copyright" content="Kaka Wan Yifan"><meta name="format-detection" content="telephone=no"><link rel="shortcut icon" href="/img/favicon.ico"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><link rel="preconnect" href="//cdn.jsdelivr.net"/><link rel="preconnect" href="https://fonts.googleapis.com" crossorigin="crossorigin"/><meta name="google-site-verification" content="SzZHj5G5vHwv9JUmJD-bxmThc7a6YoZAsaNhwcD-BmM"/><meta name="msvalidate.01" content="1DD984606A0A3BC45A692A32685321AB"/><meta name="baidu-site-verification" content="99Pgg3yv8a"/><meta name="360-site-verification" content="273e85da5f4d732881979af78473b941"/><meta name="twitter:card" content="summary"><meta name="twitter:title" content="Kaka Wan Yifan"><meta name="twitter:description" content=""><meta name="twitter:image" content="https://kakawanyifan.com/img/avatar.jpg"><meta property="og:type" content="website"><meta property="og:title" content="Kaka Wan Yifan"><meta property="og:url" content="https://kakawanyifan.com/"><meta property="og:site_name" content="Kaka Wan Yifan"><meta property="og:description" content=""><meta property="og:image" content="https://kakawanyifan.com/img/avatar.jpg"><script src="//unpkg.com/js-cookie/dist/js.cookie.min.js"></script><script>var autoChangeMode = 'false'
var t = Cookies.get("theme")
if (autoChangeMode == '1'){
var isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
var isLightMode = window.matchMedia("(prefers-color-scheme: light)").matches

【部分运行结果略】

mermaid.initialize({
theme: 'default',
})
})
}</script></body></html>
  • HttpEntity不仅可以作为返回结果,还可以作为请求参数。
  • 在判断HTTP状态码的时候,可以直接利用org.apache.http.HttpStatus的预置好的常量。
    例如:
    1
    2
    3
    if (HttpStatus.SC_OK == statusLine.getStatusCode()){

    }
  • 获取Content-Type,除了通过Headers获取,还可以通过响应体获取。
    1
    entity.getContentType();

有参

服务端

假设存在一个后台应用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {

Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println(key + ":");
System.out.println(req.getParameter(key));
}
}
}

客户端

现在,我们要以Get请求的方式传递参数。

把参数以如下的格式拼接在URL上即可

1
?【参数名-1】=【参数值-1】&【参数名-2】=【参数值-2】

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.kakawanyifan;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import java.io.IOException;

public class Get {
public static void main(String[] args) throws IOException {
// 创建http客户端
CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
// 创建get对象
String urlString = "http://localhost:8080/hcs/request";
urlString = urlString + "?" + "u=123";
System.out.println(urlString);
HttpGet httpGet = new HttpGet(urlString);
// 执行请求,获取响应体
closeableHttpClient.execute(httpGet);

if (null != closeableHttpClient){
closeableHttpClient.close();
}
}
}

运行结果:

1
http://localhost:8080/hcs/request?u=123

同时,我们会看到后台应用有打印日志:

1
2
u:
123

假如,我们传递的有中文呢?
例如,http://localhost:8080/hcs/request?u=中文字符
这也没问题,后台应用打印的日志如下:

1
2
u:
中文字符

(在《13.Servlet、Filter和Listener》,我们讨论的Get请求中的乱码的时候,提到过:8版本的Tomcat的默认配置中,已经不会复现了。)

URLEncode

但,如果有特殊符号呢?
例如,http://localhost:8080/hcs/request?u=中文字符&p=1+2+3
后台应用打印的日志如下:

1
2
3
4
u:
中文字符
p:
1 2 3

+号没了?

这是因为对于特殊字符,我们需要做urlencode

示例代码:

1
2
3
4
5
String urlString = "http://localhost:8080/hcs/request";
urlString = urlString + "?";
urlString = urlString + "u=" + URLEncoder.encode("中文字符", StandardCharsets.UTF_8.name());
urlString = urlString + "&";
urlString = urlString + "p=" + URLEncoder.encode("1+2+3", StandardCharsets.UTF_8.name());

后台打印:

1
2
3
4
u:
中文字符
p:
1+2+3

保存网络图片到本地

现在存在一张图片,地址如下:
https://kakawanyifan.com/img/avatar.jpg

现在,我们要保存网络图片到本地。
方法是,将响应体转为字节流,并写入文件。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.kakawanyifan;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.FileOutputStream;
import java.io.IOException;

public class Get {
public static void main(String[] args) throws IOException {
// 创建http客户端
CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
// 创建get对象
String urlString = "https://kakawanyifan.com/img/avatar.jpg";

HttpGet httpGet = new HttpGet(urlString);
// 执行请求,获取响应体
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);
// 获取响应体
HttpEntity httpEntity = closeableHttpResponse.getEntity();

// 获取Content-Type的值
String contentTypeValue = httpEntity.getContentType().getValue();

String suffix = ".jpg";
if (contentTypeValue.contains("jpg") || contentTypeValue.contains("jpeg")) {
suffix = ".jpg";
} else if (contentTypeValue.contains("bmp") || contentTypeValue.contains("bitmap")) {
suffix = ".bmp";
} else if (contentTypeValue.contains("png")) {
suffix = ".png";
} else if (contentTypeValue.contains("gif")) {
suffix = ".gif";
}

// 获取文件字节流
byte[] bytes = EntityUtils.toByteArray(httpEntity);
String filePath = "pic" + suffix;
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
fileOutputStream.write(bytes);
fileOutputStream.close();
}
}

设置访问代理

假设我们现在访问一个地址,该地址的访问受限,需要通过代码才能访问。

没有代理的情况,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.kakawanyifan;

import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.IOException;

public class Get {
public static void main(String[] args) throws IOException {
// 创建http客户端
CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
// 创建get对象
String urlString = "【受限地址】";

HttpGet httpGet = new HttpGet(urlString);
// 执行请求,获取响应
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);

// 获取状态码
StatusLine statusLine = closeableHttpResponse.getStatusLine();
System.out.println("状态码:" + statusLine.getStatusCode());

}
}

运行结果:

1
2
3
4
5
6
7
Exception in thread "main" org.apache.http.conn.HttpHostConnectException: Connect to 【受限地址】:443 [【受限地址】/101.230.202.141, 【受限地址】/2408:8026:b0:5d:0:0:0:2] failed: Connection timed out: connect

【部分运行结果略】

Caused by: java.net.ConnectException: Connection timed out: connect

【部分运行结果略】

添加代理,示例代码:

1
2
3
4
HttpHost proxy = new HttpHost(【代理IP】, 【代理端口】);        
RequestConfig requestConfig = RequestConfig.custom().setProxy(proxy).build();

httpGet.setConfig(requestConfig);

运行结果:

1
状态码:200

设置超时时间

在上文,我们看到没有代理的时候,报错是Connection timed out: connect
HttpClient的默认超时时间是30秒,我们也可以自定义超时时间。

超时时间一共有3种:

  • .setConnectTimeout:连接超时,单位ms,指完成tcp的3次握手的时间。
  • .setSocketTimeout:读取超时,单位ms,表示从请求的地址获取响应的时间间隔。
  • .setConnectionRequestTimeout: 从连接池获取connection的超时时间

示例代码:

1
2
3
4
5
6
7
8
9
10
// 设置超时时间
RequestConfig requestConfig = RequestConfig.custom()
// 连接超时,单位ms,指完成tcp的3次握手的时间
.setConnectTimeout(5000)
// 读取超时,单位ms,表示从请求的地址获取响应的时间间隔
.setSocketTimeout(5000)
// 从连接池获取connection的超时时间
.setConnectionRequestTimeout(5000)
.build();
httpGet.setConfig(requestConfig);

设置请求头

有时候,如果我们需要反爬,或者想防止被防盗链的话,设置请求头会有些作用。

1
2
3
4
// 设置User-Agent,伪装成浏览器
httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36");
// 防止被防盗链
httpGet.addHeader("Referer","https://www.baidu.com/");

上述的很多例子,对于Post请求,同样适用。

Post

常见的Post请求有3种:

  1. application/x-www-form-urlencoded
    以表单的形式提交。
  2. application/json
    以JSON的形式提交。
  3. multipart/form-data
    上传文件。

application/x-www-form-urlencoded

application/x-www-form-urlencoded,以表单的形式提交。

服务端

假设存在后端应用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.kakawanyifan;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
System.out.println(key + " : " + req.getHeader(key));
}

Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println(key + " : " + req.getParameter(key));
}
}
}

web.xml文件的内容如下:

1
2
3
4
5
6
7
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>

客户端

表单方式提交,主要利用NameValuePairUrlEncodedFormEntity

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.kakawanyifan;

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

public class Post {
public static void main(String[] args) throws IOException {

CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
String urlString = "http://localhost:8080/hcs/request";
HttpPost httpPost = new HttpPost(urlString);

List<NameValuePair> nameValuePairList = new ArrayList<>();
nameValuePairList.add(new BasicNameValuePair("u", "中文字符"));
nameValuePairList.add(new BasicNameValuePair("p", "1+2+3"));
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nameValuePairList, StandardCharsets.UTF_8);
httpPost.setEntity(urlEncodedFormEntity);

closeableHttpClient.execute(httpPost);

if (null != closeableHttpClient) {
closeableHttpClient.close();
}

}
}

我们会看到后台服务打印日志:

1
2
3
4
5
6
7
8
content-length : 50
content-type : application/x-www-form-urlencoded; charset=UTF-8
host : localhost:8080
connection : Keep-Alive
user-agent : Apache-HttpClient/4.5.13 (Java/1.8.0_333)
accept-encoding : gzip,deflate
u : 中文字符
p : 1+2+3

挺正常的,没啥问题,中文也没乱码。

《13.Servlet、Filter和Listener》,我们讨论乱码的时候,我们说: 因为客户端将数据发送给服务端后,并没有告诉服务端需要采取何种编码方式,这时候服务端会采取默认的ISO-8859-1。

在这里,我们的content-typeapplication/x-www-form-urlencoded; charset=UTF-8,指明了用UTF-8,所以没有乱码。

如果我们将

1
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nameValuePairList, Consts.UTF_8);

改为

1
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nameValuePairList);

即去除StandardCharsets.UTF_8,会有乱码。

1
2
3
4
5
6
7
8
content-length : 26
content-type : application/x-www-form-urlencoded
host : localhost:8080
connection : Keep-Alive
user-agent : Apache-HttpClient/4.5.13 (Java/1.8.0_333)
accept-encoding : gzip,deflate
u : ????
p : 1+2+3

如果我们用Postman默认的设置发,也有乱码,也是因为没有指明用UTF-8
Postman

此时,后台打印的日志如下:

1
2
3
4
5
6
7
8
9
10
user-agent : PostmanRuntime/7.28.4
accept : */*
postman-token : 7b5d2c32-fb0c-4d13-b4a1-e6b0d25718b0
host : localhost:8080
accept-encoding : gzip, deflate, br
connection : keep-alive
content-type : application/x-www-form-urlencoded
content-length : 50
u : 泾川文汇
p : 1+2+3

application/json

application/json,以JSON的形式提交。

服务端

此时,服务端接收方式也要改,需要以"流"的方式读取,而不是在请求体中就直接有了。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.kakawanyifan;

import com.alibaba.fastjson2.JSONObject;

import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Enumeration;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
System.out.println(key + " : " + req.getHeader(key));
}

Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
System.out.println(key + " : " + req.getParameter(key));
}

BufferedReader br = new BufferedReader(new InputStreamReader((ServletInputStream) req.getInputStream(), "utf-8"));
StringBuffer sb = new StringBuffer("");
String temp;
while ((temp = br.readLine()) != null) {
sb.append(temp);
}
br.close();

String json = sb.toString();
JSONObject jo = JSONObject.parseObject(json);
System.out.println(jo);
}
}

上述流的方式,有一种更简洁的写法:

1
2
BufferedReader reader = req.getReader();
String json = reader.readLine();

关于该部分,可以参考我们在《5.IO流》的讨论。

客户端

客户端以JSON的形式提交,主要利用StringEntity.

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.kakawanyifan;

import com.alibaba.fastjson2.JSONObject;
import org.apache.http.Consts;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import java.io.IOException;

public class Post {
public static void main(String[] args) throws IOException {

CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
String urlString = "http://localhost:8080/hcs/request";
HttpPost httpPost = new HttpPost(urlString);

JSONObject jo = new JSONObject();
jo.put("u","中文字符");
jo.put("p","1+2+3");

StringEntity stringEntity = new StringEntity(jo.toString(),Consts.UTF_8);
httpPost.setEntity(stringEntity);
closeableHttpClient.execute(httpPost);

if (null != closeableHttpClient) {
closeableHttpClient.close();
}

}
}

服务端打印:

1
2
3
4
5
6
7
content-length : 32
content-type : text/plain; charset=UTF-8
host : localhost:8080
connection : Keep-Alive
user-agent : Apache-HttpClient/4.5.13 (Java/1.8.0_333)
accept-encoding : gzip,deflate
{"u":"中文字符","p":"1+2+3"}

注意content-type : text/plain; charset=UTF-8,虽然不影响,但还是建议改掉。

1
2
3
stringEntity.setContentType("application/json; charset=utf-8");
// 设置entity的编码
stringEntity.setContentEncoding(Consts.UTF_8.name());

com.alibaba.fastjson2.JSONObject的引入:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.19</version>
</dependency>

multipart/form-data

multipart/form-data,上传文件。

服务端

服务端接收文件的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.kakawanyifan;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.Enumeration;
import java.util.List;

@WebServlet("/request")
public class RequestDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
System.out.println(key + " : " + req.getHeader(key));
}

boolean isMultipart = ServletFileUpload.isMultipartContent(req);

if (isMultipart) {
DiskFileItemFactory factory = new DiskFileItemFactory();
String path = getServletContext().getRealPath("/");
factory.setRepository(new File(path));
ServletFileUpload servletFileUpload = new ServletFileUpload(factory);

try {
// 解析请求,获取文件项
List<FileItem> fileItemList = servletFileUpload.parseRequest(req);
for (FileItem fileItem : fileItemList) {
System.out.println("fileItem :" + fileItem);
if (!fileItem.isFormField()) {
// 获取上传文件的参数
String fieldName = fileItem.getFieldName();
String fileName = fileItem.getName();
String contentType = fileItem.getContentType();

System.out.println("fieldName : " + fieldName);
System.out.println("fileName : " + fileName);
System.out.println("contentType : " + contentType);

// 写入文件
File file = new File(path + fileName);
fileItem.write(file);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

}
}

客户端

客户端通过Post上传文件。

主要利用MultipartEntityBuilder

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.kakawanyifan;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import java.io.File;
import java.io.IOException;

public class Post {
public static void main(String[] args) throws IOException {

CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
String urlString = "http://localhost:8080/hcs/request";
HttpPost httpPost = new HttpPost(urlString);

MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
multipartEntityBuilder.addBinaryBody("AttachName", new File("pic.jpg"));
HttpEntity httpEntity = multipartEntityBuilder.build();
httpPost.setEntity(httpEntity);
closeableHttpClient.execute(httpPost);

if (null != closeableHttpClient) {
closeableHttpClient.close();
}

}
}
1
2
3
4
5
6
7
8
9
10
content-length : 142661
content-type : multipart/form-data; boundary=sapnWb-jKfAsXd1DXLM5lNdOwaWchj
host : localhost:8080
connection : Keep-Alive
user-agent : Apache-HttpClient/4.5.13 (Java/1.8.0_333)
accept-encoding : gzip,deflate
fileItem : name=pic.jpg, StoreLocation=/Users/kaka/Documents/apache-tomcat-8.5.81/webapps/hcs/upload_35ec497b_9b7f_4b1d_8c80_cd465a0cc44f_00000004.tmp, size=142441 bytes, isFormField=false, FieldName=AttachName
fieldName : AttachName
fileName : pic.jpg
contentType : application/octet-stream

绕过不安全的HTTPS

如果HTTPS是安全的,那很简单,直接请求,和我们上文的讨论没有区别。

但如果是不安全的呢?
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kakawanyifan;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import java.io.IOException;

public class Get {

public static void main(String[] args) throws IOException {
CloseableHttpClient closeableHttpClient = HttpClients.createDefault();
String urlString = "【不安全的https地址】";
HttpGet httpGet = new HttpGet(urlString);
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);
System.out.println(closeableHttpResponse.getStatusLine().getStatusCode());
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)

【部分运行结果略】

at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
at com.kakawanyifan.Get.main(Get.java:14)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
at sun.security.validator.Validator.validate(Validator.java:271)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:312)
at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:221)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:128)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:636)
... 24 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
... 30 more

最常见的方法是:绕过

重写isTrusted方法,直接返回true

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.kakawanyifan;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class Get {

/**
* 构造安全连接工厂
* @return SSLConnectionSocketFactory
*/
private static ConnectionSocketFactory trustHttpsCertificates() {
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
try {
sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {
// 判断是否信任url
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
});
SSLContext sslContext = sslContextBuilder.build();
return new SSLConnectionSocketFactory(sslContext,
new String[]{"SSLv2Hello","SSLv3","TLSv1","TLSv1.1","TLSv1.2"}
,null, NoopHostnameVerifier.INSTANCE);
} catch (Exception e) {
throw new RuntimeException("构造安全连接工厂失败");
}
}

public static void main(String[] args) throws IOException {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", trustHttpsCertificates())
.build();
BasicHttpClientConnectionManager basic = new BasicHttpClientConnectionManager(registry);
HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(basic);
CloseableHttpClient closeableHttpClient = httpClientBuilder.build();
String urlString = "【不安全的https】";
HttpGet httpGet = new HttpGet(urlString);
CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet);
System.out.println(closeableHttpResponse.getStatusLine().getStatusCode());
}
}

运行结果:

1
200

连接池和工具类​

在上文,我们没有去创建默认的HttpClient,而是用HttpClients.custom().setConnectionManager(basic);去自定义了一个HttpClient。
其中basic,来自BasicHttpClientConnectionManager basic = new BasicHttpClientConnectionManager(registry);
实际上,除了BasicHttpClientConnectionManager,还有一个PoolingHttpClientConnectionManager,我们可以利用这个,创建连接池。

另外,我们可以把本章的主要代码抽出来,这样我们就了一个工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package com.kakawanyifan;

import org.apache.http.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.pool.PoolStats;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class HttpClientUtil {
private static CloseableHttpClient closeableHttpClient;
private static PoolingHttpClientConnectionManager cm;

static {
HttpClientBuilder httpClientBuilder = HttpClients.custom();
// 绕过不安全的https请求的证书验证
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", trustHttpsCertificates())
.build();
// 创建连接池管理对象
cm = new PoolingHttpClientConnectionManager(registry);
// 连接池最大有50个连接
cm.setMaxTotal(50);
// 域名(IP)+端口 就是一个路由
// 每个路由最大有多少连接
cm.setDefaultMaxPerRoute(50);
httpClientBuilder.setConnectionManager(cm);

// 设置超时时间
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(3000)
.setConnectionRequestTimeout(5000)
.build();
httpClientBuilder.setDefaultRequestConfig(requestConfig);

// 线程安全
// 此处初始化一次即可
// 通过上面的配置来生成一个用于管理多个连接的连接池closeableHttpClient
closeableHttpClient = httpClientBuilder.build();
}

/**
* 构造安全连接工厂
*
* @return SSLConnectionSocketFactory
*/
private static ConnectionSocketFactory trustHttpsCertificates() {
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
try {
sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {
// 判断是否信任url
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
});
SSLContext sslContext = sslContextBuilder.build();
return new SSLConnectionSocketFactory(sslContext,
new String[]{"SSLv2Hello", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}
, null, NoopHostnameVerifier.INSTANCE);
} catch (Exception e) {
throw new RuntimeException("构造安全连接工厂失败");
}
}

/**
* 发送get请求
*
* @param url 请求url,参数需经过URLEncode编码处理
* @param headers 自定义请求头
* @return 返回结果
*/
public static String executeGet(String url, Map<String, String> headers) {
// 构造httpGet请求对象
HttpGet httpGet = new HttpGet(url);
// 自定义请求头设置
if (headers != null) {
Set<Map.Entry<String, String>> entries = headers.entrySet();
for (Map.Entry<String, String> entry : entries) {
httpGet.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
}
}
// 可关闭的响应
CloseableHttpResponse response = null;
try {
System.out.println("prepare to execute url:" + url);
response = closeableHttpClient.execute(httpGet);
StatusLine statusLine = response.getStatusLine();
if (HttpStatus.SC_OK == statusLine.getStatusCode()) {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
} else {
System.err.println(statusLine.getStatusCode());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
consumeRes(response);
}
return null;
}

/**
* 发送表单类型的post请求
*
* @param url 要请求的url
* @param list 参数列表
* @param headers 自定义头
* @return 返回结果
*/
public static String postForm(String url, List<NameValuePair> list, Map<String, String> headers) {
HttpPost httpPost = new HttpPost(url);
if (headers != null) {
Set<Map.Entry<String, String>> entries = headers.entrySet();
for (Map.Entry<String, String> entry : entries) {
httpPost.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
}
}
// 确保请求头一定是form类型
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
// 给post对象设置参数
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(list, Consts.UTF_8);
httpPost.setEntity(formEntity);
// 响应处理
CloseableHttpResponse response = null;
try {
System.out.println("prepare to execute url:" + httpPost.getRequestLine());
response = closeableHttpClient.execute(httpPost);
StatusLine statusLine = response.getStatusLine();
if (HttpStatus.SC_OK == statusLine.getStatusCode()) {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
} else {
System.err.println(statusLine.getStatusCode());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
consumeRes(response);
}
return null;
}

/**
* 发送json类型的post请求
*
* @param url 请求url
* @param body json字符串
* @param headers 自定义header
* @return 返回结果
*/
public static String postJson(String url, String body, Map<String, String> headers) {
HttpPost httpPost = new HttpPost(url);
// 设置请求头
if (headers != null) {
Set<Map.Entry<String, String>> entries = headers.entrySet();
for (Map.Entry<String, String> entry : entries) {
httpPost.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
}
}
// 确保请求头是json类型
httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
/*
设置请求体
*/
StringEntity jsonEntity = new StringEntity(body, Consts.UTF_8);
jsonEntity.setContentType("application/json; charset=utf-8");
jsonEntity.setContentEncoding(Consts.UTF_8.name());
httpPost.setEntity(jsonEntity);

CloseableHttpResponse response = null;
try {
response = closeableHttpClient.execute(httpPost);
StatusLine statusLine = response.getStatusLine();
if (HttpStatus.SC_OK == statusLine.getStatusCode()) {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
} else {
System.err.println(statusLine.getStatusCode());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
consumeRes(response);
}
return null;
}

public static void printStat() {
// 连接池的最大连接数 50
System.out.println(cm.getMaxTotal());
// 每一个路由的最大连接数 50
System.out.println(cm.getDefaultMaxPerRoute());

PoolStats totalStats = cm.getTotalStats();
// 连接池的最大连接数 50
System.out.println(totalStats.getMax());
// 连接池里面有多少连接是被占用了
System.out.println(totalStats.getLeased());
// 连接池里面有多少连接是可用的
System.out.println(totalStats.getAvailable());
}

private static void consumeRes(CloseableHttpResponse response) {
// response.close() 是关闭连接,不是归还连接到连接池
if (response != null) {
try {
EntityUtils.consume(response.getEntity());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
  • setMaxTotal(int max):Set the maximum number of total open connections
  • setMaxPerRoute(int max):Set the total number of concurrent connections to a specific route, which is two by default。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10814
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区