开源轮子 - HTTP Client组件
HTTP组件库
文章目录
- HTTP组件库
- 一:HttpURLConnection
- 1:使用流程如下
- 2:HttpURLConnection使用的方法
- 3:get / post演示
- 4:其他说明
- 二:Apache HttpClient
- 1:获取客户端
- 2:配置参数
- 3:请求头的设置
- 4:get / post请求发送
- 5:实用功能:上传、下载
- 6:响应数据
- 6:会话保持
- 三:OkHttp
- 1:方法流程和依赖导入
- 2:get请求
- 3:post请求
- 4:完整工具类,可以直接使用
- 四:forest:声明式HTTP客户端
- 1:四步急速入门
- 2:发送JSON数据
- 3:发送XML数据
- 4:发送Protoful数据
- 5:文件上传和下载
- 6:auth验证
- 7:自定义注解
先来了解下 Java 生态中的 HTTP 组件库,大致可以分为三类:
- JDK 自带的 HttpURLConnection 标准库;
- Apache HttpComponents HttpClient;
- OkHttp。
一:HttpURLConnection
使用 HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖
缺点使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性的支持。
就像直接使用 JDBC 连接数据库那样,需要很多模板代码。
HttpURLConnection是基于http协议的,支持GET、POST、PUT、DELETE等各种请求方式。如果使用HTTPS协议请求,可以使用它的子类HttpsURLConnection完成更安全的请求操作。
1:使用流程如下
2:HttpURLConnection使用的方法
设置连接参数的方法
方法名 | 说明 |
---|---|
setAllowUserInteraction | 如果为 true ,则在允许用户交互的上下文中对此 URL 进行检查 |
setDoInput | URL 连接可用于输入和/或输出 如果打算使用 URL 连接进行输入,则将 DoInput 标志设置为 true; 如果不打算使用,则设置为 false。默认值为 true |
setDoOutput | URL 连接可用于输入和/或输出。 如果打算使用 URL 连接进行输出,则将 DoOutput 标志设置为 true; 如果不打算使用,则设置为 false。默认值为 false |
setIfModifiedSince | 有些协议支持跳过对象获取,除非该对象在某个特定时间点之后又进行了修改 |
setUseCaches | 如果为 true ,则只要有条件就允许协议使用缓存。 |
setDefaultAllowUserInteraction | 默认值为 “sticky”,它是所有 URLConnection 的其中一种静态状态。 此标志适用于下一个及后续创建的所有 URLConnection。 |
setDefaultUseCaches | 将此 URLConnection 的 useCaches 字段的值设置为指定的值。 |
设置请求头或者响应体
方法 | 说明 |
---|---|
setRequestProperty(key,value) | 设置一般请求属性。如果已存在具有该关键字的属性,则用新值改写其值。 注:HTTP 要求所有能够合法拥有多个具有相同键的实例的请求属性,使用以逗号分隔的列表语法,这样可实现将多个属性添加到一个属性中。 |
addRequestProperty(key,value) | 添加由键值对指定的一般请求属性。此方法不会改写与相同键关联的现有值 |
发送URL请求
方法 | 说明 |
---|---|
getOutputStream | 建立实际连接之后,就是发送请求,把请求参数传到服务器 这就需要使用outputStream把请求参数传给服务器 |
获取响应
方法 | 说明 |
---|---|
getContent | 检索此 URL 连接的内容; |
getHeaderField | 返回第 n 个头字段的值。如果少于 n+1 个字段,则返回 null 。此方法可与 getHeaderFieldKey 方法配合使用,以迭代消息中的所有头 |
getInputStream | 返回从此打开的连接读取的输入流。 在读取返回的输入流时,如果在数据可供读取之前达到读入超时时间,则会抛出 SocketTimeoutException。 |
getResponseCode | 获取服务器的响应代码 |
getResponseMessage | 获取服务器的响应消息 |
getResponseMethod | 获取发送请求的方法 |
相应的信息头用以下方法获取
方法 | 说明 |
---|---|
getContentEncoding | 返回 content-encoding 头字段的值 |
getContentLength | 返回 content-length 头字段的值。 |
getContentType | 返回 content-type 头字段的值。 |
getDate | 返回 date 头字段的值。 |
getExpiration | 返回 expires 头字段的值。 |
3:get / post演示
package com.example.bootrocketmq.study.wheel.httpclient;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @author cui haida
* 2024/12/24
*/
@Slf4j
public class HttpURLConnectionDemo {
public static void main(String[] args) throws IOException {
String api = "http://www.baidu.com";
getTest(api);
testDoPost();
}
public static void getTest(String api) {
HttpURLConnection connection = null;
InputStream in = null;
BufferedReader reader = null;
try {
//构造一个URL对象
URL url = new URL(api);
//获取URLConnection对象
connection= (HttpURLConnection) url.openConnection();
//getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,所以在开发中不调用connect()也可以)
in = connection.getInputStream();
//通过InputStreamReader将字节流转换成字符串,在通过BufferedReader将字符流转换成自带缓冲流
reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
//按行读取
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String response= sb.toString();
System.out.println(response);
} catch (Exception e) {
log.error("error: ", e);
} finally {
if (connection != null) {
connection.disconnect();
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error("error: ", e);
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error("error: ", e);
}
}
}
}
public static void testDoPost() throws IOException {
String apiUrl = "";
String username = "cuihaida";
String password = "123456";
String tenantUrl = "";
HttpURLConnection conn = null;
OutputStream out = null;
InputStream in = null;
String idToken = null;
try
{
// 构造一个URL对象
URL url = new URL(apiUrl);
// 获取URLConnection对象
conn = (HttpURLConnection) url.openConnection();
// 限制socket等待建立连接的时间,超时将会抛出java.net.SocketTimeoutException
conn.setConnectTimeout(3000);
// 限制输入流等待数据到达的时间,超时将会抛出java.net.SocketTimeoutException
conn.setReadTimeout(3000);
// 设定请求的方法为"POST",默认是GET
conn.setRequestMethod("POST");
// 设置传送的内容类型是json格式
conn.setRequestProperty("Content-Type", "application/json;charset=utf-8");
// 接收的内容类型也是json格式
conn.setRequestProperty("Accept", "application/json;charset=utf-8");
// 设置是否从httpUrlConnection读入,默认情况下是true
conn.setDoInput(true);
// 由于URLConnection在默认的情况下不允许输出,所以在请求输出流之前必须调用setDoOutput(true)。为一个HTTP URL将doOutput设置为true时,请求方法将由GET变为POST
conn.setDoOutput(true);
// 是否使用缓存,Post方式不能使用缓存
conn.setUseCaches(false);
// 准备数据
JSONObject json = new JSONObject();
json.put("username", username);
json.put("password", DigestUtils.md5Hex(password));
json.put("tenantUrl", tenantUrl);
// 返回一个OutputStream,可以用来写入数据传送给服务器
out = conn.getOutputStream();
// 将数据写入到输出流中
out.write(json.toString().getBytes(StandardCharsets.UTF_8));
// 刷新管道
out.flush();
// 建立连接
conn.connect();
// 判断数字响应码是否是200
int responseCode = conn.getResponseCode();
String result="";
if (responseCode == 200) {
// 获取输入流
in = conn.getInputStream();
// 获取返回的内容
StringWriter sw = new StringWriter();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
char[] buffer = new char[4096];
for (int n = 0; -1 != (n = reader.read(buffer)); ) {
sw.write(buffer, 0, n);
}
result = sw.toString();
System.out.println(result);
}
}catch (Exception exception){
exception.printStackTrace();
}finally {
if (conn != null) {
conn.disconnect();
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
}
}
}
4:其他说明
HttpURLConnection 发起的 HTTP 请求比较原始,基本上算是对网络传输层的一次浅层次的封装;
有了 HttpURLConnection 对象后,就可以获取到输出流,然后把要发送的内容发送出去;再通过输入流读取到服务器端响应的内容;最后打印。
不过 HttpURLConnection 不支持 HTTP/2.0,为了解决这个问题,Java 9 的时候官方的标准库增加了一个更高级别的 HttpClient,再发起 POST 请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。
public class HttpClientDemo {
public static void main(String[] args) throws URISyntaxException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString("牛逼"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}
}
二:Apache HttpClient
HttpClient 相比传统 JDK 自带的 URLConnection,增加了易用性和灵活性,它不仅是客户端发送 HTTP 请求变得容易,而且也方便了开发人员测试接口(基于 HTTP 协议的),即提高了开发的效率,也方便提高代码的健壮性
<!-- http client 4.X -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- 此处使用的是 5.x 版本,可以根据自身情况引入版本 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.1.1</version>
</dependency>
1:获取客户端
有下面三种方式可以获取到对应的客户端,分别如下:
// 获取默认配置的 HttpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
// 此种方式是通过 根据 系统配置创建 HttpClient
// 在项目启动时可以通过设置如下JVM启动参数:
// 1:http.agent 配置 userAgent
// 2:http.keepAlive 配置 keepAlive 数据
CloseableHttpClient httpClient = HttpClients.createSystem();
// 此种方式可以在创建时 设置一些默认值
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultHeaders(Collections.emptyList()) // 设置默认请求头
.setDefaultRequestConfig(RequestConfig.DEFAULT) // 设置默认配置
.build();
2:配置参数
HttpClient 可以通过在创建 HttpClient 对象时就设置全局配置,也可以为单个请求设置请求配置
创建配置对象
// 创建请求配置信息
RequestConfig requestConfig = RequestConfig.custom()
// 设置连接超时时间
.setConnectTimeout(Timeout.of(3000, TimeUnit.MILLISECONDS))
// 设置响应超时时间
.setResponseTimeout(3000, TimeUnit.MILLISECONDS)
// 设置从连接池获取链接的超时时间
.setConnectionRequestTimeout(3000, TimeUnit.MILLISECONDS)
.build();
全局配置
// 此种方式可以在创建时 设置一些默认值
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultHeaders(Collections.emptyList()) // 设置默认请求头
.setDefaultRequestConfig(requestConfig) // 设置默认配置
.build();
单个请求配置
// 创建 GET 请求对象
HttpGet httpGet = new HttpGet(uri);
// 设置请求参数
httpGet.setConfig(requestConfig);
3:请求头的设置
在请求时,经常会遇到设置自定义请求头,或者更改 Conent-Type 的值,可以通过如下两种方式设置:
设置公共请求头
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON));
headers.add(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, x-gzip, deflate"));
headers.add(new BasicHeader(HttpHeaders.CONNECTION, "keep-alive"));
// 创建 一个默认的 httpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setDefaultHeaders(headers) // 设置默认请求头
.build()
设置单个请求的请求头
// 创建 POST 请求
HttpPost httpPost = new HttpPost(uri);
// 添加 Content-Type 请求头
httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED);
// 添加 accept 请求头
httpPost.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
4:get / post请求发送
get请求
GET请求的所有参数是直接拼接在 URL 后面的,在 HttpClient 中 有两种方式可以实现,如下所示:
// ======= 方式一:参数直接拼接在url后面的方式=========
// 此方式表示提供了一个完整的url相当于
String name = URLEncoder.encode("张三", "utf-8");
// 请求路径及参数
String url = "http://localhost:10010/user/params?age=20&name=" + name;
// 创建 GET 请求对象
HttpGet httpGet = new HttpGet(url);
// 调用 HttpClient 的 execute 方法执行请求
CloseableHttpResponse response = httpClient.execute(httpGet);
// 获取请求状态
int code = response.getCode();
// 如果请求成功
if(code == HttpStatus.SC_OK){
LOGGER.info("响应结果为:{}", EntityUtils.toString(response.getEntity()));
}
// ======= 方式二:通过 URIBuilder 构建请求路径 =========
// 构建请求路径,及参数
URL url = new URL("http://localhost:10010/user/params");
URI uri = new URIBuilder()
.setScheme(url.getProtocol())
.setHost(url.getHost())
.setPort(url.getPort())
.setPath(url.getPath())
// 构建参数
.setParameters(
new BasicNameValuePair("name", "张三"),
new BasicNameValuePair("age", "20")
).build();
// 创建 GET 请求对象
HttpGet httpGet = new HttpGet(uri);
// 调用 HttpClient 的 execute 方法执行请求
CloseableHttpResponse response = httpClient.execute(httpGet);
// 获取请求状态
int code = response.getCode();
// 如果请求成功
if(code == HttpStatus.SC_OK){
LOGGER.info("响应结果为:{}", EntityUtils.toString(response.getEntity()));
}
post
HTTP 中的 POST 请求的数据是包含在请求体中的。在 HttpClient 中 POST 请求发送数据是通过,调用 HttpPost 类的 setEntity(HttpEntity entity) 方法设置消息内容的。
1:JSON数据的发送
// HttpClient 中发送 JSON 数据可以使用 StringHttpEntity 类实现,如下所示:
// 请求参数
String url = "http://localhost:10010/user/body";
// 创建 GET 请求对象
HttpPost httpPost = new HttpPost(url);
// 构建对象
User user = new User();
user.setName("张三")
.setAge(20)
.setAddress(new Address()
.setCounty("中国")
.setCity("北京"))
.setAihao(Arrays.asList("跑步", "爬山", "看书"));
// 创建 字符串实体对象
HttpEntity httpEntity = new StringEntity(JSON.toJSONString(user));
httpPost.setEntity(httpEntity);
// 发送 POST 请求
httpClient.execute(httpPost);
2:模拟form表单
// 创建 ContentType 对象为 form 表单模式
ContentType contentType = ContentType.create("application/x-www-form-urlencoded", StandardCharsets.UTF_8);
// 添加到 HttpPost 头中
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
// 方式一、自己拼接请求数据,并且创建 StringEntity 对象
String query = "name="+ URLEncoder.encode("张三", "utf-8") +"&age=20";
HttpEntity httpEntity = new StringEntity(query);
// 方式二、通过UrlEncodedFormEntity 创建 HttpEntity
HttpEntity httpEntity = new UrlEncodedFormEntity(
Arrays.asList(new BasicNameValuePair("name", "张三"),
new BasicNameValuePair("age", "20")),
StandardCharsets.UTF_8
);
// 把 HttpEntity 设置到 HttpPost 中
httpPost.setEntity(httpEntity);
完整代码如下:
// 创建 POST 请求对象
HttpPost httpPost = new HttpPost("http://localhost:10010/user/map");
/*String query = "name="+ URLEncoder.encode("张三", "utf-8") +"&age=20";
HttpEntity httpEntity = new StringEntity(query);*/
HttpEntity httpEntity = new UrlEncodedFormEntity(
Arrays.asList(new BasicNameValuePair("name", "张三"),
new BasicNameValuePair("age", "20")),
StandardCharsets.UTF_8
);
// 设置请求数据
httpPost.setEntity(httpEntity);
// 设置请求头
ContentType contentType = ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8);
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
// 调用 HttpClient 的 execute 方法执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
// 获取请求状态
int code = response.getCode();
// 如果请求成功
if(code == HttpStatus.SC_OK){
LOGGER.info("响应结果为:{}", EntityUtils.toString(response.getEntity()));
}
5:实用功能:上传、下载
上传
//要上传的文件
File file = new File("F:/20150703212056_Yxi4L.jpeg");
// 创建对象
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
// 添加二进制消息体
builder.addBinaryBody("file", file);
// 也可以添加文本消息
ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8);
builder.addTextBody("name", "张三", contentType);
// 通过 MultipartEntityBuilder 构建消息体
HttpEntity httpEntity = builder.build();
HttpPost httpPost = new HttpPost("http://localhost:10010/user/upload");
httpPost.setEntity(httpEntity);
CloseableHttpResponse response = httpClient.execute(httpPost);
// 获取请求状态
int code = response.getCode();
// 如果请求成功
if(code == HttpStatus.SC_OK){
LOGGER.info("响应结果为:{}", EntityUtils.toString(response.getEntity()));
}
下载
// 请求下载路径
HttpGet httpGet = new HttpGet("http://localhost:10010/user/downLoad");
CloseableHttpResponse response = httpClient.execute(httpGet);
// 如果请求成功
if (response.getCode() == HttpStatus.SC_OK){
// 获取下载文件的文件名,此处的 File-Name 头信息,需要在服务端进行自定义
Header header = response.getFirstHeader("File-Name");
String value = header.getValue();
// 读取数据
byte[] bytes = EntityUtils.toByteArray(response.getEntity());
try (OutputStream outputStream = new FileOutputStream("F:/" + value);){
outputStream.write(bytes);
outputStream.flush();
}
}
6:响应数据
在 HttpClient 中把响应封装成了 CloseableHttpResponse 对象,在此对象中可以获取如下数据:
- getCode() 获取响应状态
- getEntity() 获取响应数据
HttpClient 提供了 EntityUtils工具类,可以很好的把 响应的 HttpEntity 转换为 字节数组或者字符串
// 转换为字符串
EntityUtils.toString(response.getEntity());
// 转换为字节数组
EntityUtils.toByteArray(response.getEntity());
除了上述外,还可以在 调用 HttpClient 的 execute()方法时 传入,响应处理器,返回自定义的数据类型。如下所示,是返回一个 自定义的 Response 对象
// 自定义响应对象
@Data
@Accessors(chain = true)
class Response {
// 响应状态
private int code;
// 响应描述
private String msg;
// 响应体
private String body;
}
// 调用 execute 时自定义 响应处理类
Response execute = httpClient.execute(httpGet, response -> {
return new Response().setCode(response.getCode())
.setMsg(response.getReasonPhrase())
.setBody(EntityUtils.toString(response.getEntity(),
StandardCharsets.UTF_8));
});
6:会话保持
在实际项目中,经常会遇到需要先登录然后才能进行访问其他接口,那么, 在 HttpClient 中提供了 HttpClientContext 类,可以很好的实现,会话保持功能。
创建HttpClientContext在 execute() 方法中传入 第一步创建的对象,如下所示:
// 创建 HttpClientContext对象
HttpContext httpContext = new BasicHttpContext();
httpContext.setAttribute("name", "zhangsan");
HttpClientContext httpClientContext = HttpClientContext.adapt(httpContext);
// 登录
httpClient.execute(new HttpPost(""), httpClientContext);
// 获取数据
httpClient.execute(new HttpGet(""), httpClientContext);
三:OkHttp
OkHttp 是一个执行效率比较高的 HTTP 客户端:
- 支持 HTTP/2.0,当多个请求对应同一个 Host 地址时,可共用同一个 Socket;
- 连接池可减少请求延迟;
- 支持 GZIP 压缩,减少网络传输的数据大小;
- 支持 Response 数据缓存,避免重复网络请求;
1:方法流程和依赖导入
<!--okhttp3-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
2:get请求
get无参
/**
* 以get方式调用第三方接口
* @param url
*/
public static void doGet1(String url) throws IOException {
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
.url(url)
.get()//默认就是GET请求,可以不写
.build();
Response response = okHttpClient.newCall(request).execute();
String string = response.body().string();
System.out.println(string);
}
get有参
public static void doGet2(String url, Map<String, Object> paramMap) throws IOException {
OkHttpClient okHttpClient = new OkHttpClient();
Request.Builder requestbuilder = new Request.Builder()
.get();//默认就是GET请求,可以不写
StringBuilder urlbuilder = new StringBuilder(url);
if (Objects.nonNull(paramMap)) {
urlbuilder.append("?");
paramMap.forEach((key, value) -> {
try {
urlbuilder.append(URLEncoder.encode(key, "utf-8")).append("=").append(URLEncoder.encode((String) value, "utf-8")).append("&");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
urlbuilder.deleteCharAt(urlbuilder.length() - 1);
}
Request request = requestbuilder.url(urlbuilder.toString()).build();
Response response = okHttpClient.newCall(request).execute();
String string = response.body().string();
System.out.println(string);
}
get请求带有参数和请求头
/**
* 以get方式调用第三方接口
* @param url
*/
public static void doGet3(String url, Map<String, Object> paramMap,Map<String, String> heardMap) throws IOException {
OkHttpClient okHttpClient = new OkHttpClient();
Request.Builder requestbuilder = new Request.Builder()
.get();//默认就是GET请求,可以不写
//增加参数
StringBuilder urlbuilder = new StringBuilder(url);
if (Objects.nonNull(paramMap)) {
urlbuilder.append("?");
paramMap.forEach((key, value) -> {
try {
urlbuilder.append(URLEncoder.encode(key, "utf-8")).append("=").append(URLEncoder.encode((String) value, "utf-8")).append("&");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
urlbuilder.deleteCharAt(urlbuilder.length() - 1);
}
//增加请求头
Request.Builder heardBuilder = requestbuilder.url(urlbuilder.toString());
for (Map.Entry<String, String> stringObjectEntry : heardMap.entrySet()) {
heardBuilder.addHeader(stringObjectEntry.getKey(),stringObjectEntry.getValue());
}
Request request = heardBuilder.build();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
}
get另一种获取结果的方式
/**
* 以get方式调用第三方接口
* @param url
*/
public static void doGet(String url,Map<String, Object> paramMap,Map<String, String> heardMap) {
OkHttpClient okHttpClient = new OkHttpClient();
Request.Builder requestbuilder = new Request.Builder()
.get();//默认就是GET请求,可以不写
//增加参数
StringBuilder urlbuilder = new StringBuilder(url);
if (Objects.nonNull(paramMap)) {
urlbuilder.append("?");
paramMap.forEach((key, value) -> {
try {
urlbuilder.append(URLEncoder.encode(key, "utf-8")).append("=").append(URLEncoder.encode((String) value, "utf-8")).append("&");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
urlbuilder.deleteCharAt(urlbuilder.length() - 1);
}
//增加请求头
Request.Builder heardBuilder = requestbuilder.url(urlbuilder.toString());
for (Map.Entry<String, String> stringObjectEntry : heardMap.entrySet()) {
heardBuilder.addHeader(stringObjectEntry.getKey(),stringObjectEntry.getValue());
}
Request request = heardBuilder.build();
Call call = okHttpClient.newCall(request);
// 利用回调方式,
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println( "onFailure: ");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
}
});
}
3:post请求
post - json
/**
* post请求
* @param url
* @param json
*/
public static void doPost(String url, String json, Map<String, String> heardMap) throws IOException {
MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
String requestBody = json;
Request.Builder requestbuilder = new Request.Builder()
.url(url)
.post(RequestBody.create(mediaType, requestBody));
//增加请求头
for (Map.Entry<String, String> stringObjectEntry : heardMap.entrySet()) {
requestbuilder.addHeader(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}
Request request = requestbuilder.build();
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
}
post-params传参
/**
* post请求
* @param url
* @param json
*/
public static void doPost(String url, String json, Map<String, String> heardMap) throws IOException {
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
String requestBody = json;
Request.Builder requestbuilder = new Request.Builder()
.url(url)
.post(RequestBody.create(mediaType, requestBody));
//增加请求头
for (Map.Entry<String, String> stringObjectEntry : heardMap.entrySet()) {
requestbuilder.addHeader(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}
Request request = requestbuilder.build();
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
}
/**
* post请求
* @param url
* @param paramMap
*/
public static void doPost1(String url, Map<String, Object> paramMap,Map<String,String> heardMap) throws IOException {
FormBody.Builder formBody = new FormBody.Builder();
if (Objects.nonNull(paramMap)) {
paramMap.forEach((x, y) -> formBody.add(x, (String) y));
}
RequestBody requestBody = formBody.build();
Request.Builder requestbuilder = new Request.Builder()
.url(url)
.post(requestBody);
//增加请求头
for (Map.Entry<String, String> stringObjectEntry : heardMap.entrySet()) {
requestbuilder.addHeader(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}
Request request = requestbuilder.build();
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(request).execute();
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
}
post - form 文件
public static void doPost2(String url,File file) throws IOException {
OkHttpClient client = new OkHttpClient();
RequestBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("id", "111")
.addFormDataPart("content", "{\"do_layout\":1}")
.addFormDataPart("file", file.getName(), RequestBody.create(MediaType.parse("text/plain"), file))
.build();
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("x-tilake-app-key", "")
.addHeader("x-tilake-ca-timestamp", "")
.addHeader("x-tilake-ca-signature", "")
.addHeader("Content-Type", body.contentType().toString())
.addHeader("Accept", "*/*")
.build();
try {
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
System.out.println(response.message());
System.out.println(response.code());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
4:完整工具类,可以直接使用
package com.example.httpdemo.okhttp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.io.FileUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Slf4j
public class OkHttpUtil {
public static final String MEDIA_TYPE_JSON = "application/json; charset=utf-8";
private OkHttpUtil() {
}
/**
* 获取默认的OkHttpClient
* @return
*/
public static OkHttpClient getOkHttpClient() {
return getOkHttpClient(60, 60, 60);
}
public static OkHttpClient getOkHttpClient(int connectTimeout, int readTimeOut, int writeTimeOut) {
OkHttpClient.Builder builder = new okhttp3.OkHttpClient().newBuilder();
builder.connectTimeout(connectTimeout, TimeUnit.SECONDS);
builder.readTimeout(readTimeOut, TimeUnit.SECONDS);
builder.writeTimeout(writeTimeOut, TimeUnit.SECONDS);
return builder.build();
}
/**
* get请求
* @param okHttpClient
* @param url
* @param headers header参数
* @return
*/
public static String get(OkHttpClient okHttpClient, String url, Headers headers) {
log.info("okHttpClient get url:{}.", url);
Request request = new Request.Builder().url(url).headers(headers).get().build();
String responseData = request(okHttpClient, url, request);
log.info("okHttpClient get url:{},request responseData====> {}", url, responseData);
return responseData;
}
public static String get(OkHttpClient okHttpClient, String url) {
Headers headers = new Headers.Builder().build();
return get( okHttpClient, url, headers);
}
/**
* GET请求。使用默认的 okHttpClient 和 headers
* @param url
* @return
*/
public static String get(String url) {
OkHttpClient okHttpClient = getOkHttpClient();
Headers headers = new Headers.Builder().build();
return get( okHttpClient, url, headers);
}
/**
* post请求,获取响应结果
*
* @param okHttpClient
* @param url
* @param bodyJson
* @param headers
* @return
*/
public static String post(OkHttpClient okHttpClient, String url, JSONObject bodyJson, Headers headers) {
log.info("okHttpClient post url:{}, body====> {}", url, bodyJson);
MediaType mediaTypeJson = MediaType.parse(MEDIA_TYPE_JSON);
RequestBody requestBody = RequestBody.create(mediaTypeJson, JSON.toJSONString(bodyJson));
Request request = new Request.Builder().url(url).headers(headers).post(requestBody).build();
String responseData = request(okHttpClient, url, request);
log.info("okHttpClient post url:{},post responseData====> {}", url, responseData);
return responseData;
}
public static String post(OkHttpClient okHttpClient, String url, JSONObject bodyJson) {
Headers headers = new Headers.Builder().build();
return post( okHttpClient, url, bodyJson, headers);
}
/**
* post请求。使用默认的 okHttpClient 和 headers
* @param url
* @param bodyJson
* @return
*/
public static String post( String url, JSONObject bodyJson) {
//使用默认的 okHttpClient
OkHttpClient okHttpClient = getOkHttpClient();
Headers headers = new Headers.Builder().build();
//如果需要自定义 okHttpClient或headers传参,可以调用以下方法
return post( okHttpClient, url, bodyJson, headers);
}
/**
* 获取响应结果
*
* @param okHttpClient
* @param url
* @param request
* @return
*/
public static String request(OkHttpClient okHttpClient, String url, Request request) {
String responseData = "";
try (Response response = okHttpClient.newCall(request).execute()) {
if (response != null && response.body() != null) {
return response.body().string();
}
} catch (Exception e) {
log.error("okHttpClient getResponse error.url:{}", url, e);
}
return responseData;
}
/**
* 上传文件
*
* @param okHttpClient okHttp客户端
* @param url 上传文件的url
* @param fileKey 文件对应的key
* @param formDataJson form-data参数
* @param headers
* @param file
* @return
*/
public static String uploadFile(OkHttpClient okHttpClient, String url,
String fileKey, File file, JSONObject formDataJson, Headers headers) {
log.info("uploadFile url:{}, uploadFile formDataJson====> {}", url, formDataJson);
// 支持传文件的同时,传参数。
MultipartBody requestBody = getMultipartBody(fileKey, file, formDataJson);
// 构建request请求体
Request request = new Request.Builder().url(url).headers(headers).post(requestBody).build();
String responseData = request(okHttpClient, url, request);
// 会在本地产生临时文件,用完后需要删除
if (file.exists()) {
file.delete();
}
return responseData;
}
/**
* 上传文件
* @param url
* @param fileKey form-data文件对应的key
* @param multipartFile 文件上传对应的 multipartFile
* @param formDataJson form-data参数
* @return
*/
public static String uploadFile(String url,
String fileKey, MultipartFile multipartFile, JSONObject formDataJson) {
//使用默认的okHttpClient
OkHttpClient okHttpClient = getOkHttpClient();
Headers headers = new Headers.Builder().build();
return uploadFile(okHttpClient, url, fileKey, getFile(multipartFile), formDataJson, headers);
}
public static String uploadFile(OkHttpClient okHttpClient, String url,
String fileKey, File file, JSONObject formDataJson) {
Headers headers = new Headers.Builder().build();
return uploadFile(okHttpClient, url, fileKey, file, formDataJson, headers);
}
/**
* 上传文件
* 使用默认的okHttpClient
*
* @param url
* @param fileKey form-data文件对应的key
* @param file 文件
* @param formDataJson form-data参数
* @return
*/
public static String uploadFile(String url,
String fileKey, File file, JSONObject formDataJson) {
//使用默认的okHttpClient
OkHttpClient okHttpClient = getOkHttpClient();
Headers headers = new Headers.Builder().build();
return uploadFile(okHttpClient, url, fileKey, file, formDataJson, headers);
}
/**
* 上传文件用。构建form-data 参数
*
* @param fileKey 文件对应的key
* @param file 文件
* @param formDataJson form-data参数
* @return
*/
public static MultipartBody getMultipartBody(String fileKey, File file, JSONObject formDataJson) {
RequestBody fileBody = RequestBody.create(MultipartBody.FORM, file);
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder();
// 设置传参为form-data格式
bodyBuilder.setType(MultipartBody.FORM);
bodyBuilder.addFormDataPart(fileKey, file.getName(), fileBody);
// 添加 form-data参数
for (Map.Entry<String, Object> entry : formDataJson.entrySet()) {
//参数通过 bodyBuilder.addFormDataPart(key, value) 添加
bodyBuilder.addFormDataPart(entry.getKey(), Objects.toString(entry.getValue(),""));
}
return bodyBuilder.build();
}
/**
* 获取文件
* @param multipartFile
* @return
*/
public static File getFile(MultipartFile multipartFile) {
File file = new File(Objects.requireNonNull(multipartFile.getOriginalFilename()));
try {
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
} catch (IOException e) {
log.error("copyInputStreamToFile error.", e);
}
return file;
}
}
四:forest:声明式HTTP客户端
轻量级的 HTTP 客户端框架 Forest,正是基于 Httpclient和OkHttp 的,屏蔽了不同细节的 HTTP 组件库所带来的所有差异。
Forest 的字面意思是森林的意思,更内涵点的话,可以拆成For和Rest两个单词,也就是“为了Rest”(Rest为一种基于HTTP的架构风格)。
而合起来就是森林,森林由很多树木花草组成(可以理解为各种不同的服务),它们表面上看独立,实则在地下根茎交错纵横、相互连接依存,这样看就有点现代分布式服务化的味道了。
最后,这两个单词反过来读就像是Resultful。
项目地址:
https://gitee.com/dromara/forest
Forest 本身是处理前端过程的框架,是对后端 HTTP API 框架的进一步封装。
1:四步急速入门
1:添加依赖
<!-- forest -->
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
2:创建接口
package com.yoursite.client;
import com.dtflys.forest.annotation.Request;
import com.dtflys.forest.annotation.DataParam;
public interface AmapClient {
/**
* @Get注解代表该方法专做GET请求
* 在url中的{0}代表引用第一个参数,{1}引用第二个参数
*/
@Get("http://ditu.amap.com/service/regeo?longitude={0}&latitude={1}")
Map getLocation(String longitude, String latitude);
}
3:扫描接口
在Spring Boot的配置类或者启动类上加上@ForestScan注解,并在basePackages属性里填上远程接口的所在的包名
@SpringBootApplication
@Configuration
@ForestScan(basePackages = "com.yoursite.client")
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
4:进行bean调用
// 注入接口实例
@Autowired
private AmapClient amapClient;
...
// 调用接口
Map result = amapClient.getLocation("121.475078", "31.223577");
System.out.println(result);
2:发送JSON数据
/**
* 将对象参数解析为JSON字符串,并放在请求的Body进行传输
*/
@Post("/register")
String registerUser(@JSONBody MyUser user);
/**
* 将Map类型参数解析为JSON字符串,并放在请求的Body进行传输
*/
@Post("/test/json")
String postJsonMap(@JSONBody Map mapObj);
/**
* 直接传入一个JSON字符串,并放在请求的Body进行传输
*/
@Post("/test/json")
String postJsonText(@JSONBody String jsonText);
3:发送XML数据
/**
* 将一个通过JAXB注解修饰过的类型对象解析为XML字符串
* 并放在请求的Body进行传输
*/
@Post("/message")
String sendXmlMessage(@XMLBody MyMessage message);
/**
* 直接传入一个XML字符串,并放在请求的Body进行传输
*/
@Post("/test/xml")
String postXmlBodyString(@XMLBody String xml);
4:发送Protoful数据
/**
* ProtobufProto.MyMessage 为 Protobuf 生成的数据类
* 将 Protobuf 生成的数据对象转换为 Protobuf 格式的字节流
* 并放在请求的Body进行传输
*
* 注: 需要引入 google protobuf 依赖
*/
@Post(url = "/message", contentType = "application/octet-stream")
String sendProtobufMessage(@ProtobufBody ProtobufProto.MyMessage message);
5:文件上传和下载
/**
* 用@DataFile注解修饰要上传的参数对象
* OnProgress参数为监听上传进度的回调函数
*/
@Post("/upload")
Map upload(@DataFile("file") String filePath, OnProgress onProgress);
// 可以用一个lambda完成对上传进度的监听
Map result = myClient.upload("D:\\TestUpload\\xxx.jpg", progress -> {
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已上传百分比
if (progress.isDone()) { // 是否上传完成
System.out.println("-------- Upload Completed! --------");
}
});
/**
* 上传Map包装的文件列表,其中 {_key} 代表Map中每一次迭代中的键值
*/
@Post("/upload")
ForestRequest<Map> uploadByteArrayMap(@DataFile(value = "file", fileName = "{_key}") Map<String, byte[]> byteArrayMap);
/**
* 上传List包装的文件列表,其中 {_index} 代表每次迭代List的循环计数(从零开始计)
*/
@Post("/upload")
ForestRequest<Map> uploadByteArrayList(@DataFile(value = "file", fileName = "test-img-{_index}.jpg") List<byte[]> byteArrayList);
/**
* 在方法上加上@DownloadFile注解
* dir属性表示文件下载到哪个目录
* OnProgress参数为监听上传进度的回调函数
* {0}代表引用第一个参数
*/
@Get("http://localhost:8080/images/xxx.jpg")
@DownloadFile(dir = "{0}")
File downloadFile(String dir, OnProgress onProgress);
// 调用下载接口,可监听下载进度的百分比
File file = myClient.downloadFile("D:\\TestDownload", progress -> {
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已下载百分比
if (progress.isDone()) { // 是否下载完成
System.out.println("-------- Download Completed! --------");
}
});
6:auth验证
@Post("/hello/user?username={username}")
@BasicAuth(username = "{username}", password = "bar")
String send(@DataVariable("username") String username);
@OAuth2(
tokenUri = "/auth/oauth/token",
clientId = "password",
clientSecret = "xxxxx-yyyyy-zzzzz",
grantType = OAuth2.GrantType.PASSWORD,
scope = "any",
username = "root",
password = "xxxxxx"
)
@Get("/test/data")
String getData();
7:自定义注解
Forest允许您根据需要自行定义注解,不但让您可以简单优雅得解决各种需求,而且极大得扩展了Forest的能力。
定义一个注解
/**
* 用Forest自定义注解实现一个自定义的签名加密注解
* 凡用此接口修饰的方法或接口,其对应的所有请求都会执行自定义的签名加密过程
* 而自定义的签名加密过程,由这里的@MethodLifeCycle注解指定的生命周期类进行处理
* 可以将此注解用在接口类和方法上
*/
@Documented
/** 重点: @MethodLifeCycle注解指定该注解的生命周期类*/
@MethodLifeCycle(MyAuthLifeCycle.class)
@RequestAttributes
@Retention(RetentionPolicy.RUNTIME)
/** 指定该注解可用于类上或方法上 */
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAuth {
/**
* 自定义注解的属性:用户名
* 所有自定注解的属性可以在生命周期类中被获取到
*/
String username();
/**
* 自定义注解的属性:密码
* 所有自定注解的属性可以在生命周期类中被获取到
*/
String password();
}
定义注解生命周期类
/**
* MyAuthLifeCycle 为自定义的 @MyAuth 注解的生命周期类
* 因为 @MyAuth 是针对每个请求方法的,所以它实现自 MethodAnnotationLifeCycle 接口
* MethodAnnotationLifeCycle 接口带有泛型参数
* 第一个泛型参数是该生命周期类绑定的注解类型
* 第二个泛型参数为请求方法返回的数据类型,为了尽可能适应多的不同方法的返回类型,这里使用 Object
*/
public class MyAuthLifeCycle implements MethodAnnotationLifeCycle<MyAuth, Object> {
/**
* 当方法调用时调用此方法,此时还没有执行请求发送
* 次方法可以获得请求对应的方法调用信息,以及动态传入的方法调用参数列表
*/
@Override
public void onInvokeMethod(ForestRequest request, ForestMethod method, Object[] args) {
System.out.println("Invoke Method '" + method.getMethodName() + "' Arguments: " + args);
}
/**
* 发送请求前执行此方法,同拦截器中的一样
*/
@Override
public boolean beforeExecute(ForestRequest request) {
// 通过getAttribute方法获取自定义注解中的属性值
// getAttribute第一个参数为request对象,第二个参数为自定义注解中的属性名
String username = (String) getAttribute(request, "username");
String password = (String) getAttribute(request, "password");
// 使用Base64进行加密
String basic = "MyAuth " + Base64Utils.encode("{" + username + ":" + password + "}");
// 调用addHeader方法将加密结构加到请求头MyAuthorization中
request.addHeader("MyAuthorization", basic);
return true;
}
/**
* 此方法在请求方法初始化的时候被调用
*/
@Override
public void onMethodInitialized(ForestMethod method, BasicAuth annotation) {
System.out.println("Method '" + method.getMethodName() + "' Initialized, Arguments: " + args);
}
}
使用自定义的注解
/**
* 在请求接口上加上自定义的 @MyAuth 注解
* 注解的参数可以是字符串模板,通过方法调用的时候动态传入
* 也可以是写死的字符串
*/
@Get("/hello/user?username={username}")
@MyAuth(username = "{username}", password = "bar")
String send(@DataVariable("username") String username);
编程式请求
Forest 的编程式请求支持链式调用,极为方便、高效、简洁
// GET 请求访问百度
String baidu = Forest.get("http://www.baidu.com").execute(String.class);
// POST 请求注册用户信息
String result = Forest.post("/user/register")
.contentType("application/json")
.addBody("username", "公子骏")
.addBody("password", "12345678")
.execute(String.class);