1. 介绍
在本教程中,我们将学习如何使用在 Spring 6.1 (M2) 中引入的 RestClient
HTTP 客户端来进行 HTTP 请求。
首先,我们将了解 RestClient
API。然后,我们将看到如何使用 RestClient
API 进行 GET、POST、PUT 和 DELETE 请求。最后,我们将查看一个实际示例,如何向 OpenAI API 发送 HTTP 请求,并调用 ChatGPT 为我们生成一些文本。
2. 了解 RestClient
前段时间,Spring 框架引入了 WebClient
用于进行非阻塞的 HTTP 请求。WebClient
使用了一种响应式的范式,适用于以异步模式和流式场景下运行良好的应用程序。因此,在非响应式应用中使用它会增加不必要的复杂性,并且要求我们非常了解 Mono
和 Flux
类型。此外,WebClient
并不会让程序更高效,因为我们仍然需要等待响应。
因此,Spring 框架引入了 RestClient
API 来进行同步 HTTP 请求。这是对经典 RestTemplate
的一种更现代的方法。这并不意味着 RestTemplate
已被弃用。实际上,在底层,它们都使用相同的基础设施,如消息转换器、请求工厂等。然而,使用 RestClient
API 是一个好主意,因为它将在 Spring 框架的未来迭代中成为关注点,并会获得更多功能。
RestClient
使用诸如 JDK HttpClient、Apache HttpComponents 等 HTTP 库。它利用了类路径中可用的库。因此,RestClient 屏蔽了所有 HTTP 客户端的细节,并根据我们项目中的依赖项使用最合适的库。
3. 创建 RestClient
我们使用 create()
方法创建 RestClient
,该方法返回一个具有默认设置的新实例:
RestClient simpleRestClient = RestClient.create();
如果我们想自定义 RestClient
,可以使用 builder()
方法,并利用流畅的 API 来设置我们需要的配置:
RestClient customRestClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MappingJackson2HttpMessageConverter()))
.baseUrl("https://example.com")
.defaultHeader("Authorization", "Bearer your-token")
.defaultHeader("Content-Type", "application/json")
.build();
我们的自定义 RestClient
使用 Apache 客户端进行 HTTP 请求。然后,我们添加一个自定义消息转换器,使用 Jackson 处理 JSON 序列化和反序列化。我们为使用此客户端进行的所有请求设置基础 URL、默认头信息和内容类型。
请记住,这些配置大多已默认设置。现在,我们有一个自定义的 RestClient
,可以用来发起 HTTP 请求。由于它是线程安全的,我们可以在整个应用程序中重用这个 RestClient
实例,而不会出现问题。
我们还可以通过逐步迁移的方式从 RestTemplate
迁移到 RestClient
,并重用现有的配置来创建一个 RestClient
:
RestTemplate restTemplate = new RestTemplate();
RestClient restClientFromRestTemplate = RestClient.create(restTemplate);
4. 发起HTTP请求
在本节中,我们将简要解释如何使用 RestClient
API 进行 GET、POST、PUT 和 DELETE 请求。我们会使用一个名为 awesomeapi.com 的虚拟 API 来演示这些请求。为简单起见,我们将使用 retrieve()
方法来发起请求,并将响应获取为 String
。
4.1. 发起一个 GET 请求
最常见的HTTP请求类型是 GET 请求,我们用来从服务器获取数据。RestClient
提供了一种简单的方法来进行 GET 请求,并且可以通过各种选项来处理参数和响应类型:
String response = simpleRestClient.get()
.uri("https://awesomeapi.com/string/")
.retrieve()
.body(String.class);
record Item(String id, String name, double price) {
} // JSON: {"id": 123, "name": "Product", "price": 99.99}
Item item = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", 123)
.retrieve()
.body(Item.class);
在这个代码片段中,我们进行了两个GET请求。第一个请求只是获取一个原始的String
响应。第二个请求接收到一个Item
对象的JSON响应,该对象包含name
和price
字段。body()
方法在后台使用Jackson自动将响应反序列化为有效的Item
实例。因此,如果响应与Item
结构不同,则会抛出异常。
4.2. 发起一个 POST 请求
要向服务器发送数据,我们使用 POST 请求。更正式地说,POST 请求是在服务器上创建新的资源。正如我们在 GET 请求中看到的那样,RestClient
使得发送JSON编码对象作为请求主体变得很简单:
record ItemRequest(String name, double price) {
} // JSON: {"name": "Product", "price": 99.99}
ItemRequest newItem = new ItemRequest("Product", 99.99);
Item createdItem = simpleRestClient.post()
.uri("https://awesomeapi.com/item/")
.contentType(MediaType.APPLICATION_JSON)
.body(newItem)
.retrieve()
.body(Item.class);
在这个例子中,我们创建一个新的 ItemRequest
对象并将其发送到服务器。contentType()
方法指定我们正在发送 JSON 数据,而 body()
方法会自动将 Item
对象序列化为有效的 JSON。通常,服务器会响应其创建的资源,我们可以将其反序列化为一个 Item
对象。Item
和 ItemRequest
的区别在于,Item
具有一个由服务器生成的 id
字段。
4.3. 发起一个 PUT 请求
假设我们想要在服务器上更新一个现有资源。我们可以使用 PUT 请求来实现这一目的。与 POST 请求相似,PUT 请求包含一个我们想要更新的资源的请求体:
ItemRequest updatedItem = new ItemRequest("Updated Product", 129.99);
Item updated = simpleRestClient.put()
.uri("https://awesomeapi.com/item/{id}", 123)
.contentType(MediaType.APPLICATION_JSON)
.body(updatedItem)
.retrieve()
.body(Item.class);
PUT 请求示例展示了如何更新现有产品。我们在 uri()
方法中指定产品的 id,并在请求体中提供更新后的产品数据。响应包含服务器确认后的更新产品信息。
请注意,uri()
方法使用了路径变量语法,这是一种指定 URI 动态部分的便利方式。某些 API 使用查询参数而不是路径变量。要添加查询参数,我们可以使用 UriBuilder
,设置路径,并使用 queryParam()
方法设置参数,最后构建 Uri
以传递给 uri()
方法:
Item updatedWithQuery = simpleRestClient.put()
.uri(uriBuilder ->
uriBuilder.path("https://awesomeapi.com/item/")
.queryParam("id", 123)
.build())
.contentType(MediaType.APPLICATION_JSON)
.body(updatedItem)
.retrieve()
.body(Item.class);
查询参数在 ?
字符之后添加到 URI 中。因此,生成的 URI 将是 https://awesomeapi.com/item/?id=123
。
4.4. 发起 DELETE 请求
最后,让我们看看如何使用 DELETE 请求从服务器删除资源:
simpleRestClient.delete()
.uri("https://awesomeapi.com/item/{id}", 123)
.retrieve()
.toBodilessEntity();
在 DELETE 示例中,我们使用 toBodilessEntity()
,因为 DELETE 请求通常不返回响应体。路径变量语法指定要删除的资源。
如果删除成功,则不会抛出异常。这适用于所有请求;如果服务器响应错误状态代码,将抛出异常。我们可以使用try-catch块来处理这些异常,但还有更合理的方法来处理这些错误。我们将在下一节中看到这一点。
5. 使用 onStatus()
处理错误
当我们使用REST API时,我们需要优雅地处理错误。我们不能为每个请求都使用try-catch块,因为这会导致代码冗长且难以阅读。RestClient
提供了一种基于响应状态码处理错误的非常简洁的方法。我们使用onStatus()
方法来处理不同的状态码,并提供一种更可读的方式来处理失败:
String id = "123";
item = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new RuntimeException("Item not found with id: " + id);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new RuntimeException("Service is currently unavailable");
})
.body(Item.class);
在这个例子中,我们发起一个 GET 请求以获取具有特定 id 的 Item
对象。如果服务器响应一个 4xx 客户端错误,我们抛出一个异常,信息提示该项目在服务器上未找到。同样地,如果我们收到一个 5xx 服务器错误,我们抛出另一个 RuntimeException
,指示服务不可用。这展示了如何区分不同类型的错误;在实际场景中,我们必须根据需求实现错误处理逻辑。例如,我们可能会遇到需要
重试逻辑的临时错误。
6. 理解 retrieve()
与 exchange()
retrieve()
简化了 HTTP 请求,同时涵盖了大多数使用情况。然而,有时候我们需要对 HTTP 请求和响应进行更多的控制。例如,我们可能需要读取不适合 onStatus()
方法的响应头。在这种情况下,我们可以使用 exchange()
方法。请注意,使用 exchange()
时不会使用状态处理程序,因为它已经返回了完整的响应,允许我们执行所需的任何错误处理。
例如,我们可以使用 exchange()
实现与上一节相同的 404 错误处理:
Item itemResponse = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", id)
.exchange((request, response) -> {
HttpHeaders headers = response.getHeaders();
String etag = headers.getETag();
if (response.getStatusCode().is4xxClientError()) {
throw new RuntimeException("Item not found with id: " + id);
} else if(response.getStatusCode().is5xxServerError()){
throw new RuntimeException("Service is currently unavailable");
}
log.info("Got request with ETAG: " + etag);
return response.bodyTo(Item.class);
});
在这个例子中,我们使用if-else语句手动创建了错误处理逻辑。使用处理程序会降低代码的可读性,但允许我们访问响应中的头信息,如ETag
,而这在使用retrieve()
方法时是无法实现的。
让我们尝试一个更实际的调用真实API的例子。
7. 向 OpenAI API 发起 HTTP 请求
REST API 是与第三方服务交互的绝佳方式。在本节中,我们将使用 OpenAI API 通过 ChatGPT 模型生成一些文本。我们将使用 RestClient
向 OpenAI API 发起一个 POST 请求,并获取响应。
首先,我们需要从 OpenAI 网站获取一个 API 密钥。一个好的做法是不要将 API 密钥包含在代码库中,而是使用环境变量或其他安全方式来存储它。在这个例子中,我们假设我们有一个名为 OPENAI_API_KEY
的环境变量,其中包含了 API 密钥。要读取它,我们只需使用 System
类中的 getenv()
方法。
String apiKey = System.getenv("OPENAI_API_KEY");
接下来,我们创建一些辅助的 record
类来表示 OpenAI API 所需的请求和响应结构:
record ChatRequest(String model, List<Message> messages) {}
record Message(String role, String content) {}
record ChatResponse(List<Choice> choices) {}
record Choice(Message message) {}
现在,我们创建一个 RestClient
实例,并添加必要的认证和内容类型的请求头:
RestClient openAiClient = RestClient.builder()
.baseUrl("https://api.openai.com/v1")
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
最后,我们向 OpenAI API 发起一个 POST 请求,以使用 ChatGPT 模型生成一些文本:
ChatRequest request = new ChatRequest(
"gpt-4o-mini",
List.of(new Message("user", "What is the brightest star in our galaxy?"))
);
ChatResponse response = openAiClient.post()
.uri("/chat/completions")
.body(request)
.retrieve()
.body(ChatResponse.class);
log.info("ChatGPT: " + response.choices().get(0).message().content());
如果一切顺利,我们将看到来自ChatGPT的响应:
INFO: ChatGPT: The brightest star in our galaxy, the Milky Way, is typically considered to be **Sagittarius A***. However, strictly speaking, Sagittarius A* is actually the supermassive black hole at the center of the Milky Way. The title of the brightest visible star from Earth is generally given to **Sirius**, which is part of the constellation Canis Major. Sirius is approximately 8.6 light-years away and is about twice as massive as the Sun. It is often referred to as the "Dog Star."
这个示例展示了如何使用 RestClient
与像 OpenAI 提供的那样的真实API进行交互,以干净且类型安全的方式处理复杂的JSON结构和认证需求。
8. 结论
在这篇文章中,我们学习了 Spring Framework 6.1 中引入的新 RestClient
API,用于发起 HTTP 请求。我们学习了如何创建和配置一个 RestClient
,进行 HTTP 请求(GET、POST、PUT、DELETE),处理错误,以及如何与像 OpenAI 这样的真实世界 API 进行交互。
我们可以在 Github 上查看完整代码。