开源轮子 - HTTP Client组件

HTTP组件库

文章目录

先来了解下 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 将此 URLConnectionuseCaches 字段的值设置为指定的值。

设置请求头或者响应体

方法 说明
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演示

java 复制代码
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 请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。

java 复制代码
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 协议的),即提高了开发的效率,也方便提高代码的健壮性

xml 复制代码
<!-- 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:获取客户端

有下面三种方式可以获取到对应的客户端,分别如下:

java 复制代码
// 获取默认配置的 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 对象时就设置全局配置,也可以为单个请求设置请求配置

创建配置对象

java 复制代码
  //  创建请求配置信息
RequestConfig  requestConfig = RequestConfig.custom()
     // 设置连接超时时间
    .setConnectTimeout(Timeout.of(3000, TimeUnit.MILLISECONDS))
    // 设置响应超时时间
    .setResponseTimeout(3000, TimeUnit.MILLISECONDS) 
    // 设置从连接池获取链接的超时时间
    .setConnectionRequestTimeout(3000, TimeUnit.MILLISECONDS)
    .build();

全局配置

java 复制代码
// 此种方式可以在创建时 设置一些默认值
CloseableHttpClient  httpClient = HttpClients.custom()
                     .setDefaultHeaders(Collections.emptyList())   // 设置默认请求头
                     .setDefaultRequestConfig(requestConfig)  // 设置默认配置
                     .build();

单个请求配置

java 复制代码
// 创建 GET 请求对象
HttpGet httpGet = new HttpGet(uri);
 
// 设置请求参数
httpGet.setConfig(requestConfig);

3:请求头的设置

在请求时,经常会遇到设置自定义请求头,或者更改 Conent-Type 的值,可以通过如下两种方式设置:

设置公共请求头

java 复制代码
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()

设置单个请求的请求头

java 复制代码
// 创建 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 中 有两种方式可以实现,如下所示:

java 复制代码
// ======= 方式一:参数直接拼接在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数据的发送

java 复制代码
// 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表单

java 复制代码
// 创建 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);

完整代码如下:

java 复制代码
// 创建 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:实用功能:上传、下载

上传

java 复制代码
//要上传的文件
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()));
}

下载

java 复制代码
// 请求下载路径
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 转换为 字节数组或者字符串

java 复制代码
// 转换为字符串
EntityUtils.toString(response.getEntity());
 
// 转换为字节数组
EntityUtils.toByteArray(response.getEntity());

除了上述外,还可以在 调用 HttpClient 的 execute()方法时 传入,响应处理器,返回自定义的数据类型。如下所示,是返回一个 自定义的 Response 对象

java 复制代码
// 自定义响应对象
@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() 方法中传入 第一步创建的对象,如下所示:

java 复制代码
// 创建 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:方法流程和依赖导入

xml 复制代码
<!--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无参

java 复制代码
/**
 * 以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有参

java 复制代码
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请求带有参数和请求头

java 复制代码
/**
 * 以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另一种获取结果的方式

java 复制代码
/**
 * 以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

java 复制代码
/**
 * 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传参

java 复制代码
 /**
 * 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());
}
java 复制代码
 /**
 * 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 文件

java 复制代码
 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:完整工具类,可以直接使用

java 复制代码
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:添加依赖

xml 复制代码
<!-- forest -->
<dependency>
    <groupId>com.dtflys.forest</groupId>
    <artifactId>forest-spring-boot-starter</artifactId>
    <version>1.6.0</version>
</dependency>

2:创建接口

java 复制代码
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属性里填上远程接口的所在的包名

java 复制代码
@SpringBootApplication
@Configuration
@ForestScan(basePackages = "com.yoursite.client")
public class MyApplication {
  public static void main(String[] args) {
      SpringApplication.run(MyApplication.class, args);
   }
}

4:进行bean调用

java 复制代码
// 注入接口实例
@Autowired
private AmapClient amapClient;
...
// 调用接口
Map result = amapClient.getLocation("121.475078", "31.223577");
System.out.println(result);

2:发送JSON数据

java 复制代码
/**
 * 将对象参数解析为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数据

java 复制代码
/**
 * 将一个通过JAXB注解修饰过的类型对象解析为XML字符串
 * 并放在请求的Body进行传输
 */
@Post("/message")
String sendXmlMessage(@XMLBody MyMessage message);

/**
 * 直接传入一个XML字符串,并放在请求的Body进行传输
 */
@Post("/test/xml")
String postXmlBodyString(@XMLBody String xml);

4:发送Protoful数据

java 复制代码
/**
 * ProtobufProto.MyMessage 为 Protobuf 生成的数据类
 * 将 Protobuf 生成的数据对象转换为 Protobuf 格式的字节流
 * 并放在请求的Body进行传输
 * 
 * 注: 需要引入 google protobuf 依赖
 */
@Post(url = "/message", contentType = "application/octet-stream")
String sendProtobufMessage(@ProtobufBody ProtobufProto.MyMessage message);

5:文件上传和下载

java 复制代码
/**
 * 用@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);
java 复制代码
/**
 * 在方法上加上@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验证

java 复制代码
@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的能力。

定义一个注解

java 复制代码
/**
 * 用Forest自定义注解实现一个自定义的签名加密注解
 * 凡用此接口修饰的方法或接口,其对应的所有请求都会执行自定义的签名加密过程
 * 而自定义的签名加密过程,由这里的@MethodLifeCycle注解指定的生命周期类进行处理
 * 可以将此注解用在接口类和方法上
 */
@Documented
/** 重点: @MethodLifeCycle注解指定该注解的生命周期类*/
@MethodLifeCycle(MyAuthLifeCycle.class)
@RequestAttributes
@Retention(RetentionPolicy.RUNTIME)
/** 指定该注解可用于类上或方法上 */
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAuth {

    /** 
     * 自定义注解的属性:用户名
     * 所有自定注解的属性可以在生命周期类中被获取到
     */
    String username();

    /** 
     * 自定义注解的属性:密码
     * 所有自定注解的属性可以在生命周期类中被获取到
     */
    String password();
}

定义注解生命周期类

java 复制代码
/**
 *  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);
    }
}

使用自定义的注解

java 复制代码
/**
 * 在请求接口上加上自定义的 @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);
相关推荐
breaksoftware4 小时前
低代码开源项目Joget的研究——Joget7社区版安装部署
低代码·开源
百事可乐☆4 小时前
全局webSocket 单个页面进行监听并移除单页面监听
网络·websocket·网络协议
hgdlip4 小时前
IP属地和所在地不一致什么意思?怎么换成另外一个地方的
服务器·网络协议·tcp/ip
忆源5 小时前
Linux高级--2.4.5 靠协议头保证传输的 MAC/IP/TCP/UDP---协议帧格式
网络协议·tcp/ip·udp
笑呵呵的大文子5 小时前
论文分享—— 软件物料清单(SBOM)开源与专有工具的现状研究
开源·sbom
DashVector6 小时前
如何通过HTTP API插入Doc
数据库·人工智能·http·阿里云·向量检索
DashVector6 小时前
如何通过HTTP API分组检索Doc
服务器·数据库·http·数据库开发·数据库架构
搬砖的果果6 小时前
爬虫代理服务要怎么挑选?
网络·爬虫·网络协议·tcp/ip
畅联云平台8 小时前
美畅物联丨如何在视频汇聚平台上添加RTMP主动推流设备?
网络协议·音视频
魔众9 小时前
一个桌面工具条系统,插件一键启动,快速扩展提高工作效率
开源·php·laravel·blog