当前位置: 首页 > article >正文

从入门到精通:HttpClient深度剖析与实战指南

一、引言

1.1 背景引入

在当今数字化时代,网络编程已成为软件开发中不可或缺的一部分。而 HTTP 通信作为网络编程的核心,承担着客户端与服务器之间数据传输的重任。无论是 Web 应用、移动应用,还是分布式系统,HTTP 协议都扮演着关键角色,它使得不同设备、不同平台之间能够高效地进行数据交互。

在 Java 开发领域,为了实现 HTTP 通信,我们有众多工具可供选择,其中 Apache HttpClient 脱颖而出,成为开发者们的得力助手。HttpClient 以其强大的功能、丰富的特性以及高度的可定制性,在 Java 的 HTTP 通信场景中占据着举足轻重的地位。它不仅支持各种 HTTP 请求方法,如 GET、POST、PUT、DELETE 等,还能轻松处理复杂的请求头、响应体以及各种网络异常情况,极大地简化了 Java 开发者在 HTTP 通信方面的编程工作。

1.2 目标读者

本文主要面向对 HttpClient 感兴趣的 Java 开发者,无论你是刚刚踏入 Java 编程世界的新手,还是已经具备一定开发经验,希望深入了解 HttpClient 的进阶开发者,都能从本文中获取有价值的知识和实用的技巧。对于新手来说,本文将从基础知识入手,逐步引导你掌握 HttpClient 的使用方法;而对于有经验的开发者,本文将深入剖析 HttpClient 的原理、高级特性以及实际应用中的优化策略,帮助你进一步提升 HTTP 通信编程能力。

1.3 预期收获

通过阅读本文,读者将全面掌握 HttpClient 的基本原理,包括其工作流程、核心组件以及与 HTTP 协议的交互机制。在使用方法上,读者将学会如何创建 HttpClient 实例、发送各种类型的 HTTP 请求(GET、POST、PUT、DELETE 等),并正确处理服务器返回的响应。同时,还将了解如何设置请求头、请求体,以及处理常见的网络异常情况。

此外,本文还将深入探讨 HttpClient 在实际应用中的常见问题及解决方法,如连接超时、重定向处理、认证授权等。通过学习这些内容,读者能够在实际项目中更加灵活、高效地运用 HttpClient,提升 HTTP 通信编程的质量和效率,为开发出稳定、可靠的网络应用奠定坚实的基础。

二、HttpClient 基础认知

2.1 是什么

HttpClient 是 Apache HttpComponents 项目的重要组成部分,它是专门为创建 HTTP 客户端程序而设计的强大工具包。在 Java 开发中,当我们需要与 HTTP 服务器进行交互,发送请求并接收响应时,HttpClient 就派上了用场。它就像是一个专业的 HTTP 通信使者,能够准确无误地将我们的请求发送到服务器,并把服务器的响应带回来。

从本质上讲,HttpClient 是对 HTTP 协议的一层封装,它将 HTTP 协议中复杂的操作和细节进行了抽象,为开发者提供了一套简洁、易用的 API。通过这些 API,我们可以轻松地构建各种类型的 HTTP 请求,无论是简单的 GET 请求获取网页内容,还是复杂的 POST 请求提交表单数据、上传文件等,都能轻松实现。

2.2 为什么要用

在 Java 中,JDK 自带了一些 HTTP 访问的功能,比如HttpURLConnection。但是,与 HttpClient 相比,它就显得有些力不从心了。JDK 自带的 HTTP 访问功能虽然能够实现基本的 HTTP 请求和响应操作,但功能相对单一,使用起来也不够灵活。例如,在处理复杂的请求头设置、请求参数传递、响应数据解析等方面,HttpURLConnection的代码编写会比较繁琐,而且对于一些高级特性,如连接池管理、自动重定向处理、认证授权等,支持得也不够完善。

而 HttpClient 则弥补了这些不足。它提供了丰富的功能和灵活的配置选项,使得 HTTP 通信变得更加高效和便捷。使用 HttpClient,我们可以轻松地设置各种请求头信息,如Content-Type、Authorization等,以满足不同的业务需求。在处理请求参数时,无论是简单的键值对参数,还是复杂的 JSON、XML 格式的数据,HttpClient 都能提供方便的方法进行设置。同时,HttpClient 还内置了强大的连接池管理功能,能够有效地复用 HTTP 连接,减少连接建立和销毁的开销,提高系统的性能和稳定性。

2.3 主要功能

  1. 支持所有 HTTP 方法:HttpClient 支持 HTTP 协议中定义的所有方法,包括 GET、POST、PUT、DELETE、HEAD、OPTIONS、TRACE 等。这使得我们可以根据具体的业务需求,选择合适的 HTTP 方法来与服务器进行交互。例如,当我们需要获取服务器上的资源时,可以使用 GET 方法;当我们需要向服务器提交数据时,可以使用 POST 方法;当我们需要更新服务器上的资源时,可以使用 PUT 方法;当我们需要删除服务器上的资源时,可以使用 DELETE 方法。

  2. 自动转向:在 HTTP 通信中,服务器有时会返回重定向响应,指示客户端将请求发送到另一个 URL。HttpClient 能够自动处理这种重定向情况,按照服务器的指示自动将请求发送到新的 URL,无需我们手动编写重定向逻辑。这大大简化了我们的开发工作,确保了请求能够顺利地到达最终的目标地址。

  3. HTTPS 协议支持:随着网络安全的重要性日益凸显,HTTPS 协议被广泛应用于保障数据传输的安全。HttpClient 对 HTTPS 协议提供了全面的支持,它能够识别和验证服务器的证书,确保通信的安全性。同时,HttpClient 还支持自定义 SSL 上下文,允许我们根据具体的安全需求进行灵活配置,如信任自定义的证书颁发机构、使用双向认证等。

  4. 代理服务器支持:在一些网络环境中,我们可能需要通过代理服务器来访问外部资源。HttpClient 支持设置代理服务器,我们只需要配置代理服务器的地址和端口信息,HttpClient 就会通过代理服务器转发请求。这在企业内部网络、网络爬虫等场景中非常有用,可以帮助我们突破网络限制,实现对目标资源的访问。

三、HttpClient 的使用

3.1 环境搭建

在使用 HttpClient 之前,我们需要先将其引入到项目中。如果使用 Maven 项目管理工具,只需要在pom.xml文件中添加以下依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

在上述代码中,指定了依赖的组 ID,这里是org.apache.httpcomponents,表示这是 Apache HttpComponents 项目的依赖;指定了依赖的工件 ID,httpclient表示我们要引入的是 HttpClient 库;指定了依赖的版本号,这里使用的是4.5.13版本,你可以根据实际情况选择合适的版本。添加完依赖后,Maven 会自动下载 HttpClient 及其相关的依赖包到项目中。

3.2 基本使用步骤

以发送 GET 请求为例,展示 HttpClient 的基本使用步骤:

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClientExample {
    public static void main(String[] args) {
        // 创建HttpClient实例
        HttpClient httpClient = HttpClients.createDefault();
        // 创建HttpGet请求
        HttpGet httpGet = new HttpGet("http://example.com");
        try {
            // 执行请求
            HttpResponse response = httpClient.execute(httpGet);
            // 处理响应
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("响应内容:" + responseBody);
            } else {
                System.out.println("请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源(这里httpClient在实际应用中可能会被复用,不一定每次都关闭)
            ((CloseableHttpClient) httpClient).close();
        }
    }
}

在这段代码中,首先通过HttpClients.createDefault()方法创建了一个默认配置的HttpClient实例,这个实例就像是我们的 HTTP 通信使者,负责与服务器进行交互。接着,创建了一个HttpGet请求对象,指定了请求的 URL 为http://example.com,这个 URL 就像是我们要访问的目的地地址。然后,使用httpClient.execute(httpGet)方法执行请求,这一步就像是使者带着请求出发去访问目的地,服务器会根据请求返回相应的响应。如果响应的状态码为 200,表示请求成功,我们通过EntityUtils.toString(response.getEntity())方法获取响应体的内容,并打印出来;如果状态码不为 200,则表示请求失败,打印出失败的状态码。最后,在finally块中关闭HttpClient,释放资源,确保程序的资源管理合理。

3.3 常见请求示例

  1. GET 请求
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class GetRequestExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://example.com/api/data");
        try {
            HttpResponse response = httpClient.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("GET请求响应: " + responseBody);
            } else {
                System.out.println("GET请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ((CloseableHttpClient) httpClient).close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. POST 请求
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.util.ArrayList;
import java.util.List;

public class PostRequestExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost("http://example.com/api/upload");
        List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("key1", "value1"));
        params.add(new BasicNameValuePair("key2", "value2"));
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(params));
            HttpResponse response = httpClient.execute(httpPost);
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("POST请求响应: " + responseBody);
            } else {
                System.out.println("POST请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ((CloseableHttpClient) httpClient).close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. PUT 请求
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class PutRequestExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClients.createDefault();
        HttpPut httpPut = new HttpPut("http://example.com/api/update");
        String json = "{\"key\":\"value\"}";
        try {
            httpPut.setEntity(new StringEntity(json));
            httpPut.setHeader("Content-Type", "application/json");
            HttpResponse response = httpClient.execute(httpPut);
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("PUT请求响应: " + responseBody);
            } else {
                System.out.println("PUT请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ((CloseableHttpClient) httpClient).close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. DELETE 请求
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class DeleteRequestExample {
    public static void main(String[] args) {
        HttpClient httpClient = HttpClients.createDefault();
        HttpDelete httpDelete = new HttpDelete("http://example.com/api/delete");
        try {
            HttpResponse response = httpClient.execute(httpDelete);
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("DELETE请求响应: " + responseBody);
            } else {
                System.out.println("DELETE请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ((CloseableHttpClient) httpClient).close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3.4 参数传递

  1. GET 请求参数传递:GET 请求通过 URL 拼接参数,例如:
String baseUrl = "http://example.com/api/search";
String param1 = "value1";
String param2 = "value2";
String charset = "UTF-8";
String query = String.format("param1=%s&param2=%s",
        URLEncoder.encode(param1, charset),
        URLEncoder.encode(param2, charset));
String completeUrl = baseUrl + "?" + query;
HttpGet httpGet = new HttpGet(completeUrl);

在这段代码中,首先定义了基础 URLbaseUrl,然后准备了两个参数param1和param2。通过URLEncoder.encode方法对参数进行 URL 编码,以确保特殊字符能够正确传输。接着,使用String.format方法将参数拼接成查询字符串query,格式为param1=value1&param2=value2。最后,将查询字符串拼接到基础 URL 后面,形成完整的请求 URLcompleteUrl,并创建HttpGet请求对象。

  1. POST 请求参数传递
    • 通过表单传递参数
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("username", "testuser"));
params.add(new BasicNameValuePair("password", "testpass"));
httpPost.setEntity(new UrlEncodedFormEntity(params));

这段代码创建了一个List对象params,用于存储表单参数。通过BasicNameValuePair类将参数名和参数值封装成键值对,然后添加到params列表中。最后,使用UrlEncodedFormEntity将参数列表转换为适合 HTTP POST 请求的实体,并设置到HttpPost请求对象中。

  • 通过 JSON 传递参数
String json = "{\"name\":\"John\",\"age\":30}";
httpPost.setEntity(new StringEntity(json));
httpPost.setHeader("Content-Type", "application/json");

这里直接定义了一个 JSON 格式的字符串json,表示请求体的数据。使用StringEntity将 JSON 字符串转换为请求实体,并设置到HttpPost请求对象中。同时,设置请求头的Content-Type为application/json,告诉服务器请求体的数据格式是 JSON。

3.5 响应处理

  1. 获取响应状态码
HttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应状态码:" + statusCode);

通过response.getStatusLine().getStatusCode()方法可以获取服务器返回的响应状态码,状态码是一个三位数,用于表示请求的处理结果。例如,200 表示请求成功,404 表示资源未找到,500 表示服务器内部错误等。

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

使用response.getAllHeaders()方法可以获取响应头的所有信息,返回一个Header数组。通过遍历这个数组,可以获取每个响应头的名称和值,并进行相应的处理。

  1. 获取响应体
if (response.getStatusLine().getStatusCode() == 200) {
    String responseBody = EntityUtils.toString(response.getEntity());
    System.out.println("响应内容:" + responseBody);
}

当响应状态码为 200 时,表示请求成功,可以通过EntityUtils.toString(response.getEntity())方法获取响应体的内容。EntityUtils类是 HttpClient 提供的工具类,用于处理响应实体,将其转换为字符串形式以便于处理。

  1. 处理不同类型的响应
    • JSON 响应处理:可以使用 JSON 解析库,如 Jackson、Gson 等,将响应体的 JSON 字符串解析为 Java 对象。例如,使用 Gson 库:
import com.google.gson.Gson;

if (response.getStatusLine().getStatusCode() == 200) {
    String responseBody = EntityUtils.toString(response.getEntity());
    Gson gson = new Gson();
    MyResponseObject obj = gson.fromJson(responseBody, MyResponseObject.class);
    System.out.println("解析后的对象:" + obj);
}

这里首先获取响应体的 JSON 字符串responseBody,然后创建一个Gson对象。使用gson.fromJson方法将 JSON 字符串解析为MyResponseObject类型的 Java 对象,MyResponseObject是根据响应数据结构定义的 Java 类,用于映射 JSON 数据。

  • XML 响应处理:可以使用 XML 解析库,如 JAXB、DOM4J 等,将响应体的 XML 字符串解析为 Java 对象或文档对象模型(DOM)。例如,使用 JAXB 库:
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

if (response.getStatusLine().getStatusCode() == 200) {
    String responseBody = EntityUtils.toString(response.getEntity());
    JAXBContext jaxbContext = JAXBContext.newInstance(MyResponseXml.class);
    Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
    MyResponseXml xmlObj = (MyResponseXml) jaxbUnmarshaller.unmarshal(new StringReader(responseBody));
    System.out.println("解析后的XML对象:" + xmlObj);
}

这段代码首先获取响应体的 XML 字符串responseBody,然后创建JAXBContext对象,指定要解析的 Java 类MyResponseXml。通过JAXBContext创建Unmarshaller对象,使用unmarshal方法将 XML 字符串解析为MyResponseXml类型的 Java 对象。

四、HttpClient 原理剖析

4.1 核心组件

  1. HttpClient 实例:它是整个 HttpClient 框架的核心,负责与服务器进行通信。通过HttpClient实例,我们可以发送各种类型的 HTTP 请求,如 GET、POST、PUT、DELETE 等。HttpClient实例就像是一个经验丰富的探险家,能够根据我们的指示,准确地前往服务器获取或提交数据。在实际应用中,我们通常会创建一个HttpClient实例,并在多个请求中复用它,以减少资源的消耗。

  2. HttpRequest:它代表一个 HTTP 请求,包括请求的方法(GET、POST 等)、URL、请求头和请求体等信息。HttpRequest就像是我们给探险家的任务清单,明确了需要访问的地址、使用的方法以及携带的参数等信息。例如,HttpGet和HttpPost都是HttpRequest的具体实现类,分别用于表示 GET 请求和 POST 请求。

  3. HttpResponse:它表示 HTTP 响应,包含了服务器返回的状态码、响应头和响应体等信息。HttpResponse就像是探险家从服务器带回的 “宝藏”,我们可以从中获取服务器对请求的处理结果,如状态码 200 表示请求成功,404 表示资源未找到等。通过解析响应头和响应体,我们可以获取服务器返回的数据、处理结果以及其他相关信息。

  4. HttpClient 执行器:它负责执行HttpRequest,并将服务器返回的响应封装成HttpResponse。HttpClient执行器就像是探险家的 “交通工具”,负责将请求发送到服务器,并将响应带回给我们。在执行请求的过程中,它会处理请求的发送、接收以及连接管理等工作,确保请求能够顺利完成。

4.2 请求执行流程

  1. 创建 HttpClient 实例
HttpClient httpClient = HttpClients.createDefault();

这一步创建了一个默认配置的HttpClient实例,它就像是为我们的 HTTP 通信之旅准备了一艘坚固的 “船只”,具备基本的航行能力。

\2. 创建请求对象:以 GET 请求为例:

HttpGet httpGet = new HttpGet("http://example.com");

这里创建了一个HttpGet请求对象,指定了请求的 URL 为http://example.com,就像是为船只设定了航行的目的地。

\3. 执行请求

HttpResponse response = httpClient.execute(httpGet);

通过httpClient.execute(httpGet)方法执行请求,此时HttpClient实例就像船长一样,指挥着船只朝着目的地前进,将请求发送到服务器,并等待服务器返回响应。在这个过程中,HttpClient会处理一系列的底层操作,如建立 TCP 连接、发送 HTTP 请求报文、接收 HTTP 响应报文等。

\4. 处理响应

if (response.getStatusLine().getStatusCode() == 200) {
    String responseBody = EntityUtils.toString(response.getEntity());
    System.out.println("响应内容:" + responseBody);
} else {
    System.out.println("请求失败,状态码:" + response.getStatusLine().getStatusCode());
}

当接收到服务器的响应后,首先检查响应的状态码。如果状态码为 200,表示请求成功,通过EntityUtils.toString(response.getEntity())方法获取响应体的内容,并进行相应的处理,就像是打开探险家带回的宝藏,查看其中的内容;如果状态码不为 200,则表示请求失败,打印出失败的状态码,以便我们了解请求失败的原因。

4.3 连接管理

  1. 连接池概念和作用:连接池是一种缓存机制,它可以预先创建并管理一定数量的 HTTP 连接。当我们需要发送 HTTP 请求时,不需要每次都重新建立连接,而是从连接池中获取一个已有的连接,使用完毕后再将连接放回连接池。连接池就像是一个停车场,里面停放着许多可用的 “车辆”(连接),我们可以随时从停车场中租用车辆,使用完后再归还,这样可以避免频繁地创建和销毁连接,减少资源的开销,提高系统的性能和效率。在高并发的场景下,连接池的作用尤为明显,它可以有效地复用连接,减少连接建立的时间和资源消耗,从而提高系统的吞吐量。

  2. 配置和使用连接池

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200); // 设置最大连接数
cm.setDefaultMaxPerRoute(20); // 设置每个路由的最大连接数
CloseableHttpClient httpClient = HttpClients.custom()
      .setConnectionManager(cm)
      .build();

在这段代码中,首先创建了一个PoolingHttpClientConnectionManager对象cm,它是HttpClient连接池的管理器。通过cm.setMaxTotal(200)方法设置连接池的最大连接数为 200,这意味着连接池中最多可以同时存在 200 个连接;通过cm.setDefaultMaxPerRoute(20)方法设置每个路由的最大连接数为 20,路由可以理解为目标服务器的地址,每个目标服务器最多可以使用 20 个连接。然后,使用HttpClients.custom()创建一个HttpClient构建器,并通过.setConnectionManager(cm)将连接池管理器设置到构建器中,最后通过.build()方法构建出一个使用连接池的CloseableHttpClient实例。这样,在使用这个HttpClient实例发送请求时,就会从连接池中获取连接,实现连接的复用。

五、HttpClient 高级应用

5.1 自定义 HttpClient

在实际应用中,我们常常需要根据具体的业务需求对 HttpClient 进行个性化配置,以满足不同场景下的 HTTP 通信要求。通过 HttpClient.Builder 类,我们可以轻松实现这一目标,下面将详细介绍如何设置超时、代理、重定向策略等。

设置超时

超时设置是非常重要的,它可以避免请求因为长时间等待而导致程序阻塞。HttpClient 支持设置多种超时时间,包括连接超时、读取超时和从连接池获取连接的超时。

import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class CustomHttpClientExample {
    public static void main(String[] args) {
        // 设置超时时间
        RequestConfig requestConfig = RequestConfig.custom()
              .setConnectTimeout(5000) // 连接超时时间,5秒
              .setSocketTimeout(10000) // 读取超时时间,10秒
              .setConnectionRequestTimeout(3000) // 从连接池获取连接的超时时间,3秒
              .build();

        // 创建自定义的HttpClient
        CloseableHttpClient httpClient = HttpClients.custom()
              .setDefaultRequestConfig(requestConfig)
              .build();

        // 这里可以进行请求操作,例如发送GET请求
        // HttpGet httpGet = new HttpGet("http://example.com");
        // HttpResponse response = httpClient.execute(httpGet);
        // 处理响应等操作
    }
}

在上述代码中,首先通过RequestConfig.custom()创建一个RequestConfig构建器,然后使用.setConnectTimeout(5000)设置连接超时时间为 5000 毫秒,即 5 秒,表示在尝试连接到服务器时,如果超过 5 秒还未建立连接,则抛出连接超时异常;.setSocketTimeout(10000)设置读取超时时间为 10000 毫秒,即 10 秒,意味着在连接建立后,从服务器读取数据时,如果超过 10 秒还未读取到数据,则抛出读取超时异常;.setConnectionRequestTimeout(3000)设置从连接池获取连接的超时时间为 3000 毫秒,即 3 秒,当从连接池中获取连接时,如果等待时间超过 3 秒还未获取到可用连接,则抛出获取连接超时异常。最后,通过HttpClients.custom().setDefaultRequestConfig(requestConfig).build()创建一个使用自定义请求配置的CloseableHttpClient实例。

设置代理

当我们的应用程序需要通过代理服务器访问目标服务器时,可以通过以下方式进行设置。

import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class ProxyHttpClientExample {
    public static void main(String[] args) {
        // 设置代理服务器
        HttpHost proxy = new HttpHost("proxy.example.com", 8080, "http");

        // 设置请求配置,包含代理
        RequestConfig requestConfig = RequestConfig.custom()
              .setProxy(proxy)
              .build();

        // 创建自定义的HttpClient
        CloseableHttpClient httpClient = HttpClients.custom()
              .setDefaultRequestConfig(requestConfig)
              .build();

        // 这里可以进行请求操作,例如发送GET请求
        // HttpGet httpGet = new HttpGet("http://example.com");
        // HttpResponse response = httpClient.execute(httpGet);
        // 处理响应等操作
    }
}

在这段代码中,首先创建一个HttpHost对象proxy,指定代理服务器的地址为proxy.example.com,端口为 8080,协议为http。然后,在创建RequestConfig时,通过.setProxy(proxy)将代理设置到请求配置中。最后,使用包含代理配置的RequestConfig创建CloseableHttpClient实例。这样,当使用这个HttpClient发送请求时,请求会通过指定的代理服务器转发到目标服务器。

设置重定向策略

HttpClient 默认会自动处理重定向,但有时我们可能需要自定义重定向策略,以满足特殊的业务需求。

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

public class CustomRedirectStrategyExample {
    public static void main(String[] args) {
        // 创建自定义的重定向策略
        // 这里只是示例,实际可以根据需求实现更复杂的重定向逻辑
        // 例如只允许特定域名的重定向,或者根据响应头进行条件重定向等
        // 下面是一个简单的不允许重定向的示例
        // 注意,这里只是简单展示自定义重定向策略的设置方式,实际应用中可能需要更复杂的逻辑
        // 例如根据业务规则判断是否允许重定向,以及如何处理重定向等
        // 可以参考官方文档和相关资料,深入了解重定向策略的实现和应用
        RequestConfig requestConfig = RequestConfig.custom()
              .setRedirectsEnabled(false)
              .build();

        // 创建自定义的HttpClient
        CloseableHttpClient httpClient = HttpClients.custom()
              .setDefaultRequestConfig(requestConfig)
              .build();

        HttpGet httpGet = new HttpGet("http://example.com");
        HttpContext context = HttpClientContext.create();

        try {
            HttpResponse response = httpClient.execute(httpGet, context);
            if (response.getStatusLine().getStatusCode() == 200) {
                String responseBody = EntityUtils.toString(response.getEntity());
                System.out.println("响应内容:" + responseBody);
            } else {
                System.out.println("请求失败,状态码:" + response.getStatusLine().getStatusCode());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                httpClient.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

在这个示例中,通过RequestConfig.custom().setRedirectsEnabled(false)设置不允许重定向,即当服务器返回重定向响应时,HttpClient 不会自动进行重定向操作。如果需要实现更复杂的重定向策略,可以自定义实现RedirectStrategy接口,并在创建RequestConfig时通过.setRedirectStrategy(customRedirectStrategy)进行设置。

5.2 异步请求

在某些场景下,同步请求可能会导致线程阻塞,影响程序的性能和响应速度。而异步请求则可以避免这种情况,它允许在发送请求后,程序继续执行其他任务,而无需等待服务器的响应。当服务器响应到达时,通过回调函数或其他机制来处理响应结果。这样可以大大提高程序的并发处理能力和用户体验。

下面展示如何使用 HttpClient 的sendAsync()方法发送异步请求:

import org.apache.http.HttpResponse;
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.util.concurrent.CompletableFuture;

public class AsyncHttpClientExample {
    public static void main(String[] args) {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://example.com");

        CompletableFuture<HttpResponse> future = httpClient.executeAsync(httpGet);
        future.thenApply(response -> {
            try {
                return EntityUtils.toString(response.getEntity());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).thenAccept(System.out::println)
             .exceptionally(Throwable::printStackTrace);
    }
}

在上述代码中,首先创建了一个CloseableHttpClient实例和一个HttpGet请求对象。然后,使用httpClient.executeAsync(httpGet)方法发送异步请求,该方法返回一个CompletableFuture对象future,它代表了异步操作的结果。通过future.thenApply(response -> {… })方法,对异步操作的结果(即HttpResponse)进行处理,将响应体转换为字符串。接着,使用thenAccept(System.out::println)方法,将转换后的字符串打印输出。最后,通过exceptionally(Throwable::printStackTrace)方法,处理异步操作过程中可能抛出的异常,将异常堆栈信息打印出来。这样,在发送请求后,主线程不会被阻塞,可以继续执行其他任务,当服务器响应到达时,会自动调用相应的回调函数来处理响应和异常。

5.3 与其他框架集成

以 Spring 框架为例,展示如何在 Spring 项目中集成 HttpClient,实现 HTTP 通信。在 Spring 项目中集成 HttpClient 可以充分利用 Spring 的依赖注入和配置管理功能,使代码更加简洁、可维护。

首先,在pom.xml文件中添加 HttpClient 的依赖:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

然后,创建一个服务类,用于发送 HTTP 请求:

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;

@Service
public class HttpService {
    public String sendGetRequest(String url) {
        HttpClient httpClient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);
        try {
            HttpResponse response = httpClient.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == 200) {
                return EntityUtils.toString(response.getEntity());
            } else {
                return "请求失败,状态码:" + response.getStatusLine().getStatusCode();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "请求发生异常:" + e.getMessage();
        }
    }
}

在上述代码中,创建了一个HttpService服务类,并使用@Service注解将其标记为一个服务组件,以便 Spring 容器进行管理。在sendGetRequest方法中,创建了一个默认的HttpClient实例和一个HttpGet请求对象,指定请求的 URL。然后,执行请求并处理响应,如果响应状态码为 200,则返回响应体的内容;否则,返回请求失败的状态码信息。如果在请求过程中发生异常,捕获异常并返回异常信息。

最后,在控制器中调用该服务:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HttpController {
    @Autowired
    private HttpService httpService;

    @GetMapping("/http/{url}")
    public String sendHttpGetRequest(@PathVariable String url) {
        return httpService.sendGetRequest(url);
    }
}

在HttpController控制器类中,使用@Autowired注解自动注入HttpService服务。通过@GetMapping(“/http/{url}”)注解定义了一个 HTTP GET 请求的映射路径,其中{url}是一个路径变量,表示要请求的 URL。在sendHttpGetRequest方法中,接收路径变量url,并调用httpService.sendGetRequest(url)方法发送 HTTP GET 请求,将请求结果返回给客户端。这样,在 Spring 项目中就实现了 HttpClient 的集成,通过控制器调用服务类中的方法,实现了 HTTP 通信功能。

六、HttpClient 常见问题及解决

6.1 缺少证书问题

在使用 HttpClient 进行 HTTPS 通信时,有时会遇到缺少证书的问题,这通常会导致sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target异常。这个异常的出现是因为 HttpClient 在验证服务器的 SSL 证书时,无法找到有效的证书路径,也就是说,它不信任服务器提供的证书。

解决这个问题可以尝试从网站下载证书,然后将其添加到项目中。具体步骤如下:

  1. 首先,我们需要找到一个可以下载证书的工具,例如InstallCert.java。你可以从相关的技术网站上获取这个工具,比如从https://confluence.atlassian.com/download/attachments/180292346/InstallCert.java下载。

  2. 下载完成后,编译InstallCert.java。假设你已经安装了 Java 开发环境,在命令行中进入到InstallCert.java所在的目录,执行javac InstallCert.java命令进行编译。

  3. 编译成功后,执行java InstallCert hostname,其中hostname是目标服务器的域名,比如java InstallCert www.163.com。按照提示操作,这个过程会在当前目录下生成一个名为 “ssecacerts” 的证书。

  4. 最后,将生成的证书拷贝到$JAVA_HOME/jre/lib/security目录下,这样 HttpClient 在进行 HTTPS 通信时就能够信任这个证书,从而解决缺少证书的问题。

6.2 上传文件问题

在实际应用中,经常会遇到需要通过 HttpClient 上传文件的场景。使用 post 请求上传文件时,我们可以借助org.apache.httpcomponents的httpmime jar 包来实现。

请求负载是文件的情形

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 org.apache.http.util.EntityUtils;

import java.io.File;

public class FileUploadExample {
    public static void main(String[] args) {
        String url = "http://example.com/upload";
        String fileName = "test.txt";
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpPost httpPost = new HttpPost(url);
            FileBody file = new FileBody(new File(fileName));
            HttpEntity reqEntity = MultipartEntityBuilder.create()
                 .addPart("myfile", file)
                 .build();
            httpPost.setEntity(reqEntity);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            try {
                if (response.getStatusLine().getStatusCode() == 200) {
                    System.out.println("文件上传成功");
                } else {
                    System.out.println("文件上传失败,状态码:" + response.getStatusLine().getStatusCode());
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这段代码中,我们首先创建了一个HttpPost请求对象,指定了上传的 URL。然后,创建了一个FileBody对象,它代表要上传的文件。接着,使用MultipartEntityBuilder来构建请求实体,将文件添加到请求实体中,这里的 “myfile” 是文件在服务器端接收时的参数名。最后,设置请求实体到HttpPost对象中,并执行请求。

请求负载中有字符串的情形

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.io.File;
import java.nio.charset.Charset;

public class FileAndStringUploadExample {
    public static void main(String[] args) {
        String url = "http://example.com/upload";
        String fileName = "test.txt";
        String paramValue = "testParam";
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpPost httpPost = new HttpPost(url);
            FileBody file = new FileBody(new File(fileName));
            StringBody id = new StringBody(paramValue, Charset.forName("UTF-8"));
            HttpEntity reqEntity = MultipartEntityBuilder.create()
                 .addPart("scheduleId", id)
                 .addPart("myfile", file)
                 .build();
            httpPost.setEntity(reqEntity);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            try {
                if (response.getStatusLine().getStatusCode() == 200) {
                    System.out.println("文件和参数上传成功");
                } else {
                    System.out.println("文件和参数上传失败,状态码:" + response.getStatusLine().getStatusCode());
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,除了要上传的文件外,还需要传递一个字符串参数。我们创建了一个StringBody对象来表示这个参数,设置其值和字符编码。然后,将StringBody和FileBody都添加到请求实体中,“scheduleId” 是字符串参数在服务器端接收时的参数名,“myfile” 是文件参数名。这样就可以同时上传文件和字符串参数了。

6.3 POST 请求不是键值对的形式

一般情况下,post 请求的负载中常常是键值对的形式,但在某些特殊场景下,我们可能会遇到 POST 请求不是键值对的情况。比如,当我们需要发送 JSON 格式的数据或者其他自定义格式的数据时,就不能简单地按照键值对的方式来构建请求。

此时,我们可以通过设置请求实体来实现。以发送 JSON 数据为例:

import org.apache.http.HttpEntity;
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 org.apache.http.util.EntityUtils;

public class NonKeyValuePostExample {
    public static void main(String[] args) {
        String url = "http://example.com/api";
        String json = "{\"key\":\"value\",\"name\":\"John\"}";
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpPost post = new HttpPost(url);
            post.setHeader("Accept", "application/json");
            post.setHeader("Content-Type", "application/json");
            post.setEntity(new StringEntity(json));
            CloseableHttpResponse response = httpClient.execute(post);
            try {
                if (response.getStatusLine().getStatusCode() == 200) {
                    String responseBody = EntityUtils.toString(response.getEntity());
                    System.out.println("响应内容:" + responseBody);
                } else {
                    System.out.println("请求失败,状态码:" + response.getStatusLine().getStatusCode());
                }
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这段代码中,首先创建了一个HttpPost请求对象,指定请求的 URL。然后,设置请求头的Accept和Content-Type都为application/json,表示我们期望接收和发送的数据格式都是 JSON。接着,创建一个StringEntity对象,将 JSON 字符串作为参数传入,这个StringEntity就是我们的请求实体。最后,将请求实体设置到HttpPost对象中,并执行请求。通过这种方式,就可以实现发送非键值对形式的 POST 请求,满足不同的业务需求。

七、最佳实践与优化建议

7.1 性能优化

  1. 使用连接池:在高并发场景下,频繁创建和销毁 HTTP 连接会消耗大量的系统资源和时间。连接池可以预先创建一定数量的连接,并在需要时复用这些连接,从而减少连接建立和销毁的开销,提高系统的性能和响应速度。例如,使用PoolingHttpClientConnectionManager来创建连接池,并设置合适的最大连接数和每个路由的最大连接数:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200); // 设置最大连接数为200
cm.setDefaultMaxPerRoute(20); // 设置每个路由的默认最大连接数为20
CloseableHttpClient httpClient = HttpClients.custom()
     .setConnectionManager(cm)
     .build();
  1. 设置合理的超时时间:合理设置连接超时、读取超时和从连接池获取连接的超时时间非常重要。如果超时时间设置过长,可能会导致请求长时间等待,影响系统的响应性能;如果设置过短,可能会导致一些正常的请求因为短暂的网络延迟而失败。例如:
RequestConfig requestConfig = RequestConfig.custom()
     .setConnectTimeout(5000) // 连接超时时间为5秒
     .setSocketTimeout(10000) // 读取超时时间为10秒
     .setConnectionRequestTimeout(3000) // 从连接池获取连接的超时时间为3秒
     .build();
CloseableHttpClient httpClient = HttpClients.custom()
     .setDefaultRequestConfig(requestConfig)
     .build();
  1. 优化请求参数:在发送请求时,尽量减少不必要的请求参数,避免传输大量无用的数据。同时,对于一些需要频繁发送的请求,可以考虑对请求参数进行缓存,避免重复计算和生成。例如,如果某个请求的参数在一段时间内不会发生变化,可以将这些参数缓存起来,下次请求时直接使用缓存的参数,而不需要重新计算和设置。

7.2 代码规范

  1. 异常处理:在使用 HttpClient 发送请求时,可能会遇到各种异常,如网络异常、连接超时、协议异常等。为了保证程序的稳定性和可靠性,需要对这些异常进行妥善处理。使用try-catch块捕获异常,并根据不同的异常类型进行相应的处理。例如:
try {
    HttpResponse response = httpClient.execute(httpGet);
    // 处理响应
} catch (IOException e) {
    System.err.println("请求发生I/O异常:" + e.getMessage());
} catch (Exception e) {
    System.err.println("请求发生其他异常:" + e.getMessage());
}
  1. 资源释放:在使用完 HttpClient 相关资源后,一定要及时释放,避免资源泄漏。对于CloseableHttpClient、CloseableHttpResponse等实现了Closeable接口的对象,使用try-with-resources语句或在finally块中调用close()方法来关闭资源。例如:
try (CloseableHttpClient httpClient = HttpClients.createDefault();
     CloseableHttpResponse response = httpClient.execute(httpGet)) {
    // 处理响应
} catch (IOException e) {
    e.printStackTrace();
}
  1. 代码复用:将常用的 HttpClient 操作封装成独立的方法或类,提高代码的复用性。比如,创建一个专门的 HttpUtil 类,将发送 GET 请求、POST 请求等操作封装成静态方法,在其他地方需要使用时直接调用这些方法,避免重复编写相同的代码。这样不仅可以减少代码量,还便于维护和修改。

7.3 安全注意事项

  1. 认证和授权:在进行 HTTP 通信时,如果涉及到敏感数据或需要保护的资源,一定要进行认证和授权。可以使用基本认证、摘要认证、OAuth 等方式来实现认证和授权。例如,使用基本认证:
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
        new AuthScope("example.com", 80),
        new UsernamePasswordCredentials("username", "password"));
CloseableHttpClient httpClient = HttpClients.custom()
     .setDefaultCredentialsProvider(credsProvider)
     .build();
  1. 防止 SQL 注入:如果 HttpClient 请求的数据会用于数据库操作,一定要注意防止 SQL 注入。避免直接将用户输入的数据拼接到 SQL 语句中,而是使用参数化查询或预编译语句。例如,在 Java 中使用PreparedStatement来执行 SQL 查询,将用户输入的数据作为参数传递,而不是直接拼接到 SQL 语句中,这样可以有效防止 SQL 注入攻击。

  2. 防止 XSS 攻击:如果 HttpClient 请求返回的数据会在网页上展示,要注意防止跨站脚本(XSS)攻击。对返回的数据进行严格的过滤和转义,避免恶意脚本注入。可以使用一些安全的 HTML 解析库或工具,对返回的数据进行过滤和净化,确保数据在展示时不会被浏览器解析为恶意脚本。例如,使用 OWASP 的 Java Encoder 库对数据进行编码,将特殊字符进行转义,防止 XSS 攻击。

八、总结与展望

8.1 总结回顾

在本文中,我们深入探讨了 HttpClient 这一强大的 Java HTTP 客户端工具。从基础认知出发,了解到 HttpClient 是 Apache HttpComponents 项目的重要组成部分,专门用于创建 HTTP 客户端程序,它支持所有 HTTP 方法,具备自动转向、HTTPS 协议支持以及代理服务器支持等强大功能。在使用方面,我们详细学习了其环境搭建、基本使用步骤、常见请求示例、参数传递以及响应处理等内容。通过实际代码示例,我们掌握了如何创建 HttpClient 实例,发送 GET、POST、PUT、DELETE 等请求,并对服务器返回的响应进行有效的处理。

在原理剖析部分,我们深入研究了 HttpClient 的核心组件,包括 HttpClient 实例、HttpRequest、HttpResponse 以及 HttpClient 执行器,了解了它们在 HTTP 通信中的各自职责。同时,详细解析了请求执行流程,从创建 HttpClient 实例、创建请求对象、执行请求到处理响应,每一个步骤都至关重要。此外,还探讨了连接管理中的连接池概念和作用,以及如何配置和使用连接池来提高系统性能。

在高级应用方面,我们学习了如何自定义 HttpClient,包括设置超时、代理、重定向策略等,以满足不同场景下的业务需求。同时,还掌握了异步请求的使用方法,通过sendAsync()方法实现了非阻塞的 HTTP 请求,提高了程序的并发处理能力。此外,以 Spring 框架为例,展示了如何在实际项目中集成 HttpClient,实现与其他框架的协同工作。

针对常见问题,我们也进行了详细的分析和解决。在缺少证书问题上,通过从网站下载证书并添加到项目中的方法,解决了 HttpClient 在 HTTPS 通信中遇到的证书信任问题。在上传文件问题上,根据请求负载的不同情况,分别展示了如何上传文件以及同时上传文件和字符串参数的方法。对于 POST 请求不是键值对的形式,通过设置请求实体,成功实现了发送 JSON 格式数据等非键值对形式的 POST 请求。

在最佳实践与优化建议部分,我们从性能优化、代码规范和安全注意事项三个方面入手,提出了一系列的优化建议。在性能优化方面,使用连接池、设置合理的超时时间以及优化请求参数等方法,能够有效提高系统的性能和响应速度。在代码规范方面,合理的异常处理、及时的资源释放以及代码复用,能够提高代码的稳定性和可维护性。在安全注意事项方面,进行认证和授权、防止 SQL 注入以及防止 XSS 攻击等措施,能够确保 HTTP 通信的安全性。


http://www.kler.cn/a/519619.html

相关文章:

  • SpringBoot--基本使用(配置、整合SpringMVC、Druid、Mybatis、基础特性)
  • Maven的继承与聚合特性:大型项目管理的利器
  • 【Linux笔记】Day5
  • Vue 3 30天精进之旅:Day 04 - 计算属性与侦听器
  • Labview替代平台ATECLOUD的兼容性如何?
  • Docker常用知识点问题
  • K8S中的数据存储之基本存储
  • 共同建设:GAEA 社区如何塑造 AI 的未来
  • 2024年度总结(具身智能赛道,欢迎交流)
  • Mysql 默认隔离级别分布式锁乐观锁
  • JAVAweb学习日记(八) 请数据库模型MySQL
  • ray.rllib-入门实践-11: 自定义模型/网络
  • 第22章 走进xUnit:测试驱动开发的关键工具(持续探索)
  • 凝“华”聚智,“清”创未来-----华清远见教育科技集团成都中心2024年度总结大会暨2025新春盛典
  • 【论文阅读】HumanPlus: Humanoid Shadowing and Imitation from Humans
  • 蓝桥杯之c++入门(一)【第一个c++程序】
  • 27. 【.NET 8 实战--孢子记账--从单体到微服务】--简易报表--报表服务
  • Docker 系列之 docker-compose 容器编排详解
  • 【信息系统项目管理师-选择真题】2017上半年综合知识答案和详解
  • Transfoemr的解码器(Decoder)与分词技术