简介
在Java中,HttpClient
,一般指的指Apache的那一款HttpClient
。
官网:http://hc.apache.org
迄今为止,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 = new URL(urlString); URLConnection urlConnection = url.openConnection(); 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 { CloseableHttpClient closeableHttpClient = HttpClients.createDefault(); 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 { CloseableHttpClient closeableHttpClient = HttpClients.createDefault(); 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
同时,我们会看到后台应用有打印日志:
假如,我们传递的有中文呢?
例如,http://localhost:8080/hcs/request?u=中文字符
。
这也没问题,后台应用打印的日志如下:
(在《13.Servlet、Filter和Listener》 ,我们讨论的Get请求中的乱码
的时候,提到过:8版本的Tomcat的默认配置中,已经不会复现了。)
URLEncode
但,如果有特殊符号呢?
例如,http://localhost:8080/hcs/request?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());
后台打印:
保存网络图片到本地
现在存在一张图片,地址如下:
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 { CloseableHttpClient closeableHttpClient = HttpClients.createDefault(); String urlString = "https://kakawanyifan.com/img/avatar.jpg" ; HttpGet httpGet = new HttpGet(urlString); CloseableHttpResponse closeableHttpResponse = closeableHttpClient.execute(httpGet); HttpEntity httpEntity = closeableHttpResponse.getEntity(); 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 { CloseableHttpClient closeableHttpClient = HttpClients.createDefault(); 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);
运行结果:
设置超时时间
在上文,我们看到没有代理的时候,报错是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() .setConnectTimeout(5000 ) .setSocketTimeout(5000 ) .setConnectionRequestTimeout(5000 ) .build(); httpGet.setConfig(requestConfig);
设置请求头
有时候,如果我们需要反爬,或者想防止被防盗链的话,设置请求头会有些作用。
1 2 3 4 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请求有3种:
application/x-www-form-urlencoded
以表单的形式提交。
application/json
以JSON的形式提交。
multipart/form-data
上传文件。
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 >
客户端
表单方式提交,主要利用NameValuePair
和UrlEncodedFormEntity
。
示例代码:
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-type
,application/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
。
此时,后台打印的日志如下:
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" ); 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
,上传文件。
服务端
服务端接收文件的示例代码:
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 { private static ConnectionSocketFactory trustHttpsCertificates () { SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); try { sslContextBuilder.loadTrustMaterial(null , new TrustStrategy() { @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()); } }
运行结果:
连接池和工具类
在上文,我们没有去创建默认的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(); Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http" , PlainConnectionSocketFactory.INSTANCE) .register("https" , trustHttpsCertificates()) .build(); cm = new PoolingHttpClientConnectionManager(registry); cm.setMaxTotal(50 ); cm.setDefaultMaxPerRoute(50 ); httpClientBuilder.setConnectionManager(cm); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000 ) .setSocketTimeout(3000 ) .setConnectionRequestTimeout(5000 ) .build(); httpClientBuilder.setDefaultRequestConfig(requestConfig); closeableHttpClient = httpClientBuilder.build(); } private static ConnectionSocketFactory trustHttpsCertificates () { SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); try { sslContextBuilder.loadTrustMaterial(null , new TrustStrategy() { @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 String executeGet (String url, Map<String, String> headers) { 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 ; } 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())); } } httpPost.addHeader("Content-Type" , "application/x-www-form-urlencoded; charset=utf-8" ); 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 ; } 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())); } } 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 () { System.out.println(cm.getMaxTotal()); System.out.println(cm.getDefaultMaxPerRoute()); PoolStats totalStats = cm.getTotalStats(); System.out.println(totalStats.getMax()); System.out.println(totalStats.getLeased()); System.out.println(totalStats.getAvailable()); } private static void consumeRes (CloseableHttpResponse response) { 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。