SmallRye GraphQL Client
本指南展示了 Quarkus 应用程序如何使用 GraphQL 客户端库。客户端由 SmallRye GraphQL项目实现。本指南专门针对客户端,因此如果您需要了解 GraphQL 的一般知识,请首先参考SmallRye GraphQL guide,其中介绍了 GraphQL 查询语言、一般概念和服务器端开发。
This guide demonstrates how your Quarkus application can use the GraphQL client library. The client is implemented by the SmallRye GraphQL project. This guide is specifically geared towards the client side, so if you need an introduction to GraphQL in general, first refer to the SmallRye GraphQL guide, which provides an introduction to the GraphQL query language, general concepts and server-side development.
本指南将指导您开发和运行一个简单的应用程序,该应用程序使用支持的两种 GraphQL 客户端类型从远程资源(与星球大战相关的数据库)中检索数据。如果您想手动对其进行试验,可以在 this webpage处找到它。Web UI 允许您针对它编写和执行 GraphQL 查询。
The guide will walk you through developing and running a simple application that uses both supported types of GraphQL clients to retrieve data from a remote resource, that being a database related to Star Wars. It’s available at this webpage if you want to experiment with it manually. The web UI allows you to write and execute GraphQL queries against it.
Prerequisites
Unresolved directive in smallrye-graphql-client.adoc - include::{includes}/prerequisites.adoc[]
GraphQL client types introduction
支持两种类型的 GraphQL 客户端。
Two types of GraphQL clients are supported.
*typesafe*客户端的工作方式非常像针对调用 GraphQL 端点而调整的 MicroProfile REST Client。客户端实例基本上是一个代理,您可以像常规 Java 对象一样调用它,但在底层,调用将被转换为 GraphQL 操作。它直接与域类一起使用。用于该操作的任何输入和输出对象都将被转换为/从其在 GraphQL 查询语言中的表示形式。
The typesafe client works very much like the MicroProfile REST Client adjusted for calling GraphQL endpoints. A client instance is basically a proxy that you can call like a regular Java object, but under the hood, the call will be translated to a GraphQL operation. It works with domain classes directly. Any input and output objects for the operation will be translated to/from their representations in the GraphQL query language.
另一方面,*dynamic*客户端的工作方式更像是来自`jakarta.ws.rs.client`包的 Jakarta REST 客户端的等效项。它不需要域类就能工作,而是使用 GraphQL 文档的抽象表示形式。文档使用特定于领域的语言 (DSL) 构建。交换的对象被视为抽象的`JsonObject`,但是,在必要时,可以将它们转换为具体的模型对象(如果存在合适的模型类)。
The dynamic client, on the other hand, works rather like an equivalent of the Jakarta REST client
from the jakarta.ws.rs.client
package. It does not require the domain classes to work, it works with
abstract representations of GraphQL documents instead. Documents are built using a domain-specific language (DSL).
The exchanged objects are treated as an abstract JsonObject
, but, when necessary,
it is possible to convert them to concrete model objects (if suitable model classes are available).
类型安全客户端可以被视为设计为易于使用的相当高级别的声明方法,而动态客户端则处于更低级别,更命令式,使用起来有些冗长,但允许对操作和响应进行更细粒度的控制。
The typesafe client can be viewed as a rather high-level and more declarative approach designed for ease of use, whereas the dynamic client is lower-level, more imperative, somewhat more verbose to use, but allows finer grained control over operations and responses.
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
克隆 Git 存储库: git clone {quickstarts-clone-url}
,或下载 {quickstarts-archive-url}[存档]。
Clone the Git repository: git clone {quickstarts-clone-url}
, or download an {quickstarts-archive-url}[archive].
解决方案位于`microprofile-graphql-client-quickstart` directory中。
The solution is located in the microprofile-graphql-client-quickstart
directory.
Creating the Maven Project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
First, we need a new project. Create a new project with the following command:
Unresolved directive in smallrye-graphql-client.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个导入`smallrye-graphql-client`扩展的项目,也导入`rest-jsonb`扩展,因为我们也将使用它 - REST 端点将成为允许您手动触发 GraphQL 客户端来完成工作的入口点。
This command generates a project, importing the smallrye-graphql-client
extension, and also the rest-jsonb
extension, because we will use that too - a REST endpoint will be the entrypoint to allow you to manually trigger
the GraphQL client to do its work.
如果您已经配置了 Quarkus 项目,则可以通过在项目基本目录中运行以下命令向项目添加`smallrye-graphql-client`扩展:
If you already have your Quarkus project configured, you can add the smallrye-graphql-client
extension
to your project by running the following command in your project base directory:
Unresolved directive in smallrye-graphql-client.adoc - include::{includes}/devtools/extension-add.adoc[]
这会将以下内容添加到构建文件中:
This will add the following to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-graphql-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-graphql-client")
The application
我们将构建的应用程序使用这两种类型的 GraphQL 客户端。在这两种情况下,它们都将连接至 SWAPI处的星球大战服务并查询星球大战电影列表以及每部电影中出现的星球名称。
The application we will build makes use of both types of GraphQL clients. In both cases, they will connect to the Star Wars service at SWAPI and query it for a list of Star Wars films, and, for each film, the names of the planets which appear in that film.
对应的 GraphQL 查询如下所示:
The corresponding GraphQL query looks like this:
{
allFilms {
films {
title
planetConnection {
planets {
name
}
}
}
}
}
您可以访问 the webpage 手动执行此查询。
You may go to the webpage to execute this query manually.
Using the Typesafe client
要使用类型安全的客户端,我们需要与模式兼容的相应模型类。获取它们的方法有两种。首先是使用 SmallRye GraphQL 提供的客户端生成器,该生成器从模式文档和查询列表生成类。此生成器目前被认为是高度实验性的,本示例中不介绍该生成器。如果您感兴趣,请参阅 Client Generator 和其文档。
To use the typesafe client, we need the corresponding model classes that are compatible with the schema. There are two ways to obtain them. First is to use the client generator offered by SmallRye GraphQL, which generates classes from the schema document and a list of queries. This generator is considered highly experimental for now, and is not covered in this example. If interested, refer to the Client Generator and its documentation.
在此示例中,我们将手动创建精简版模型类,仅保留我们需要的字段,并忽略所有不需要的字段。我们需要 Film
和 Planet
的类。但该服务还使用名为 FilmConnection
和 PlanetConnection
的特定包装器,对我们而言,它们的作用只是分别包含 Film
和 Planet
实例的实际列表。
In this example, we will create a slimmed down version of the model classes manually, with only the fields
that we need, and ignore all the stuff that we don’t need. We will need the classes for Film
and Planet
.
But, the service is also using specific wrappers named FilmConnection
and PlanetConnection
, which,
for our purpose, will serve just to contain the actual list of Film
and Planet
instances, respectively.
让我们创建所有模型类并将它们放入 org.acme.microprofile.graphql.client.model
包中:
Let’s create all the model classes and put them into the org.acme.microprofile.graphql.client.model
package:
public class FilmConnection {
private List<Film> films;
public List<Film> getFilms() {
return films;
}
public void setFilms(List<Film> films) {
this.films = films;
}
}
public class Film {
private String title;
private PlanetConnection planetConnection;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public PlanetConnection getPlanetConnection() {
return planetConnection;
}
public void setPlanetConnection(PlanetConnection planetConnection) {
this.planetConnection = planetConnection;
}
}
public class PlanetConnection {
private List<Planet> planets;
public List<Planet> getPlanets() {
return planets;
}
public void setPlanets(List<Planet> planets) {
this.planets = planets;
}
}
public class Planet {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
现在有了模型类,我们可以创建表示我们想要在远程 GraphQL 服务上调用的实际操作集的界面。
Now that we have the model classes, we can create the interface that represents the actual set of operations we want to call on the remote GraphQL service.
@GraphQLClientApi(configKey = "star-wars-typesafe")
public interface StarWarsClientApi {
FilmConnection allFilms();
}
为简单起见,我们只调用名为 allFilms
的查询。我们也为相应的方法命名为 allFilms
。如果我们给方法命名不同,则需要使用 @Query(value="allFilms")
对其进行注释,以指定在调用此方法时应执行的查询的名称。
For simplicity, we’re only calling the query named allFilms
. We named our corresponding method
allFilms
too. If we named the method differently, we would need to annotate it with
@Query(value="allFilms")
to specify the name of the query that should be executed when this
method is called.
客户端还需要一些配置,即至少需要远程服务的 URL。我们可以指定在 @GraphQLClientApi
注释中(通过设置 endpoint
参数)指定该 URL,或将其移到配置文件 application.properties
中:
The client also needs some configuration, namely at least the URL of the remote service. We can either
specify that within the @GraphQLClientApi
annotation (by setting the endpoint
parameter),
or move this over to the configuration file, application.properties
:
quarkus.smallrye-graphql-client.star-wars-typesafe.url=https://swapi-graphql.netlify.app/.netlify/functions/index
在 tests only 期间,URL 是一个可选属性,如果未指定该属性,则 Quarkus 将假设客户端的目标是正在测试的应用程序(通常为 |
During tests only, the URL is an optional property, and if it’s not specified, Quarkus will assume
that the target of the client is the application that is being tested (typically, |
star-wars-typesafe
是已配置客户端实例的名称,并且对应于 @GraphQLClientApi
注释中的 configKey
。如果你不想指定自定义名称,则可以省略 configKey
,然后使用该接口的限定名称来引用它。
star-wars-typesafe
is the name of the configured client instance, and corresponds to the configKey
in the @GraphQLClientApi
annotation. If you don’t want to specify a custom name, you can leave
out the configKey
, and then refer to it by using the fully qualified name of the interface.
现在我们已正确配置客户端实例,我们需要一个在启动应用程序时执行某些操作的方法。为此,我们将使用一个 REST 端点,当用户调用该端点时,它会获取客户端实例并让其执行查询。
Now that we have the client instance properly configured, we need a way to have it perform something when we start the application. For that, we will use a REST endpoint that, when called by a user, obtains the client instance and lets it execute the query.
@Path("/")
public class StarWarsResource {
@Inject
StarWarsClientApi typesafeClient;
@GET
@Path("/typesafe")
@Produces(MediaType.APPLICATION_JSON)
@Blocking
public List<Film> getAllFilmsUsingTypesafeClient() {
return typesafeClient.allFilms().getFilms();
}
}
如果应用程序中包含此 REST 端点,则你可以简单地向 /typesafe
发送 GET 请求,并且应用程序将使用注入的类型安全客户端实例来调用远程服务、获取电影和星球,并返回结果列表的 JSON 表示形式。
With this REST endpoint included in your application, you can simply send a GET request to /typesafe
,
and the application will use an injected typesafe client instance to call the remote service, obtain
the films and planets, and return the JSON representation of the resulting list.
Logging
出于调试目的,可以通过将 io.smallrye.graphql.client
类别的日志级别更改为 TRACE
来记录类型安全客户端生成的请求和服务器发回的响应(有关如何配置日志记录的更多详细信息,请参阅 Logging guide)。
For debugging purpose, it is possible to log the request generated by the typesafe client and the response sent back by the server by changing the log level of the io.smallrye.graphql.client
category to TRACE
(see the Logging guide for more details about how to configure logging).
这可以通过向 application.properties
中添加以下行来实现:
This can be achieved by adding the following lines to the application.properties
:
quarkus.log.category."io.smallrye.graphql.client".level=TRACE
quarkus.log.category."io.smallrye.graphql.client".min-level=TRACE
Using the Dynamic client
对于动态客户端,模型类是可选的,因为我们可以使用 GraphQL 类型和文档的抽象表示。根本不需要客户端 API 接口。
For the dynamic client, the model classes are optional, because we can work with abstract representations of the GraphQL types and documents. The client API interface is not needed at all.
我们仍然需要为客户端配置 URL,因此,让我们将此值放入 application.properties
中:
We still need to configure the URL for the client, so let’s put this into application.properties
:
quarkus.smallrye-graphql-client.star-wars-dynamic.url=https://swapi-graphql.netlify.app/.netlify/functions/index
我们决定将客户端命名为 star-wars-dynamic
。当注入动态客户端以正确限定注入点时,我们将使用此名称。
We decided to name the client star-wars-dynamic
. We will use this name when injecting a dynamic client
to properly qualify the injection point.
如果你需要添加授权标头或任何其他自定义 HTTP 标头(在我们的情况下这是必需的),可以通过以下方式执行此操作:
If you need to add an authorization header, or any other custom HTTP header (in our case it’s not required), this can be done by:
quarkus.smallrye-graphql-client.star-wars-dynamic.header.HEADER-KEY=HEADER-VALUE"
将其添加到前面创建的 StarWarsResource
中:
Add this to the StarWarsResource
created earlier:
import static io.smallrye.graphql.client.core.Document.document;
import static io.smallrye.graphql.client.core.Field.field;
import static io.smallrye.graphql.client.core.Operation.operation;
// ....
@Inject
@GraphQLClient("star-wars-dynamic") (1)
DynamicGraphQLClient dynamicClient;
@GET
@Path("/dynamic")
@Produces(MediaType.APPLICATION_JSON)
@Blocking
public List<Film> getAllFilmsUsingDynamicClient() throws Exception {
Document query = document( (2)
operation(
field("allFilms",
field("films",
field("title"),
field("planetConnection",
field("planets",
field("name")
)
)
)
)
)
);
Response response = dynamicClient.executeSync(query); 3
return response.getObject(FilmConnection.class, "allFilms").getFilms(); 4
}
1 | Qualifies the injection point so that we know which named client needs to be injected here. |
2 | Here we build a document representing the GraphQL query, using the provided DSL language. We use static imports to make the code easier to read. The DSL is designed in a way that it looks quite similar to writing a GraphQL query as a string. |
3 | Execute the query and block while waiting for the response. There is also an asynchronous
variant that returns a Uni<Response> . |
4 | Here we did the optional step of converting the response to instances of our model classes,
because we have the classes available. If you don’t have the classes available or don’t want to
use them, simply calling response.getData() would get you a JsonObject representing
all the returned data. |
Running the application
使用以下命令在 dev 模式下启动应用程序:
Launch the application in dev mode using:
Unresolved directive in smallrye-graphql-client.adoc - include::{includes}/devtools/dev.adoc[]
要执行查询,您需要向我们的 REST 端点发送 GET 请求:
To execute the queries, you need to send GET requests to our REST endpoint:
curl -s http://localhost:8080/dynamic # to use the dynamic client
curl -s http://localhost:8080/typesafe # to use the typesafe client
无论您使用动态还是类型安全,结果都应该相同。如果 JSON 文档难以读取,您可能需要通过一种工具,例如通过管道将输出通过 jq
进行格式化,以使其更易于人类阅读。
Whether you use dynamic or typesafe, the result should be the same.
If the JSON document is hard to read, you might want to run it through a tool that
formats it for better readability by humans, for example by piping the output through jq
.