Getting Started With Reactive
_Reactive_是一组构建强大、高效且并行应用程序和系统的原则。这些原则允许您处理比传统方法更多负载,同时更有效地使用资源(CPU 和内存),同时按部就班地处理故障。
Reactive is a set of principles to build robust, efficient, and concurrent applications and systems. These principles let you handle more load than traditional approaches while using the resources (CPU and memory) more efficiently while also reacting to failures gracefully.
Quarkus 是一款 _Reactive_框架。从一开始,_Reactive_就一直是 Quarkus 架构的一项重要原则。它包括许多反应式特性并提供了广泛的生态系统。
Quarkus is a Reactive framework. Since the beginning, Reactive has been an essential tenet of the Quarkus architecture. It includes many reactive features and offers a broad ecosystem.
本指南并非关于什么是 _Reactive_以及 Quarkus 如何启用反应式架构的深入文章。如果您希望了解更多有关这些主题的信息,请参阅 Reactive Architecture guide,其中提供了 Quarkus 反应式生态系统的概述。
This guide is not an in-depth article about what Reactive is and how Quarkus enables reactive architectures. If you want to read more about these topics, refer to the Reactive Architecture guide, which provides an overview of the Quarkus reactive ecosystem.
在本指南中,我们将带您入门 Quarkus 的一些反应式特性。我们将实现一个简单的 CRUD 应用程序。然而,与 Hibernate with Panache guide不同,它使用 Quarkus 的反应式特性。
In this guide, we will get you started with some reactive features of Quarkus. We are going to implement a simple CRUD application. Yet, unlike in the Hibernate with Panache guide, it uses the reactive features of Quarkus.
本指南将帮助您:
This guide will help you with:
-
Bootstrapping a reactive CRUD application with Quarkus
-
Using Hibernate Reactive with Panache to interact with a database in a reactive fashion
-
Using Quarkus REST (formerly RESTEasy Reactive) to implement HTTP API while enforcing the reactive principle
-
Packaging and Running the application
Prerequisites
Unresolved directive in getting-started-reactive.adoc - include::{includes}/prerequisites.adoc[]
验证 Maven 是否正在使用您期望的 Java 版本。如果您安装了多个 JDK,请确保 Maven 正在使用预期的 JDK。您可以通过运行 `mvn --version.`验证 Maven 使用的 JDK |
Verify that Maven is using the Java version you expect.
If you have multiple JDKs installed, make sure Maven is using the expected one.
You can verify which JDK Maven uses by running |
Imperative vs. Reactive: a question of threads
如上所述,在本指南中,我们将实现一个反应式 CRUD 应用程序。但您可能想知道它与传统命令模型相比有何不同和优势。
As mentioned above, in this guide, we are going to implement a reactive CRUD application. But you may wonder what the differences and benefits are in comparison to the traditional and imperative model.
为更好地理解这种对比,我们需要解释反应式和命令执行模型之间的差异。理解 _Reactive_不仅仅是不同的执行模型至关重要,但为了理解本指南,这种区分非常必要。
To better understand the contrast, we need to explain the difference between the reactive and imperative execution models. It’s essential to comprehend that Reactive is not just a different execution model, but that distinction is necessary to understand this guide.
在传统的命令方法中,框架分配了一个线程来处理请求。因此,请求的整个处理都在此工作线程上运行。该模型扩展性不是很强。事实上,要处理多个并发请求,您需要多个线程;因此,您应用程序的并发性受到线程数量的限制。此外,代码一旦与远程服务交互,这些线程就会被阻塞。因此,这会导致低效地使用资源,因为您可能需要更多线程,而每个线程(由于它们被映射到 OS 线程)在内存和 CPU 方面都有成本。
In the traditional and imperative approach, frameworks assign a thread to handle the request. So, the whole processing of the request runs on this worker thread. This model does not scale very well. Indeed, to handle multiple concurrent requests, you need multiple threads; and so your application concurrency is constrained by the number of threads. In addition, these threads are blocked as soon as your code interacts with remote services. So, it leads to inefficient usage of the resources, as you may need more threads, and each thread, as they are mapped to OS threads, has a cost in terms of memory and CPU.
另一方面,反应式模型依赖于非阻塞 I/O 和不同的执行模型。非阻塞 I/O 提供了一种处理并发 I/O 的有效方法。称为 I/O 线程的线程量极小,可以处理许多并发 I/O。使用这种模型,请求处理不会委托给工作线程,而是直接使用这些 I/O 线程。由于不需要创建工作线程来处理请求,因此节省了内存和 CPU。它还提高了并发性,因为它消除了线程数量的限制。最后,它还缩短了响应时间,因为它减少了线程切换的数量。
On the other side, the reactive model relies on non-blocking I/Os and a different execution model. Non-blocking I/O provides an efficient way to deal with concurrent I/O. A minimal amount of threads called I/O threads, can handle many concurrent I/O. With such a model, request processing is not delegated to a worker thread but uses these I/O threads directly.It saves memory and CPU as there is no need to create worker threads to handle the requests. It also improves the concurrency as it removes the constraint on the number of threads. Finally, it also improves response time as it reduces the number of thread switches.
From sequential to continuation style
因此,在反应式执行模型中,请求是使用 I/O 线程处理的。但事实并非如此。I/O 线程可以处理多个并发请求。该怎么做呢?这是反应式和命令式之间的技巧和最显着的差异之一。
So, with the reactive execution model, the requests are processed using I/O threads. But that’s not all. An I/O thread can handle multiple concurrent requests. How? Here is the trick and one of the most significant differences between reactive and imperative.
在处理请求涉及与远程服务互动时,例如 HTTP API 或数据库,它不会在等待响应时阻塞执行。相反,它调度 I/O 操作并附加一个延续操作,即处于请求处理中的代码。此延续操作可以通过回调传递(使用 I/O 结果调用的函数),或使用更高级的构造,如反应式编程或协程。无论如何表达延续操作,释放 I/O 线程都是一个关键方面,随后这个线程可用于处理其他请求。在计划的 I/O 完成后,I/O 线程将执行延续操作,并且继续处理暂挂的请求。
When processing a request requires interacting with a remote service, like an HTTP API or a database, it does not block the execution while waiting for the response. Instead, it schedules the I/O operation and attaches a continuation, i.e., the request processing remaining code. This continuation can be passed as a callback (a function invoked with the I/O outcome), or use more advanced constructs such as reactive programming or co-routines. Regardless of how the continuation is expressed, the essential aspect is the release of the I/O thread and, as a consequence, the fact that this thread can be used to process another request. When the scheduled I/O completes, the I/O thread executes the continuation, and the processing of the pending request continues.
因此,与 I/O 阻塞执行命令式模型不同,反应式模型切换到基于延续操作的设计,其中会释放 I/O 线程,并且在 I/O 完成时调用延续操作。因此,I/O 线程可处理多个并发请求,改善应用程序的整体并发性。
So, unlike the imperative model, where I/O blocks the execution, reactive switches to a continuation-based design, where the I/O threads are released, and continuation invoked when the I/Os complete. As a result, the I/O thread can handle multiple concurrent requests, improving the overall concurrency of the application.
但是,有一个问题。我们需要一个方法来编写延续传递代码。有许多方式可用来实现此目的。在 Quarkus 中,我们提出:
But, there is a catch. We need a way to write continuation-passing code. There are many ways of doing this. In Quarkus, we propose:
-
Mutiny - an intuitive and event-driven reactive programming library
-
Kotlin co-routines - a way to write asynchronous code in a sequential manner
在该指南中我们将使用 Mutiny。如需了解有关 Mutiny 的更多信息,请查看 Mutiny documentation。
In this guide, we will use Mutiny. To know more about Mutiny, check the Mutiny documentation.
Project Loom 将很快进入 JDK 中并提出了基于虚拟线程的模型。只要 Loom 普遍可用,Quarkus 架构就准备好支持它。 |
Project Loom is coming to the JDK soon and proposes a virtual thread-based model. The Quarkus architecture is ready to support Loom as soon as it’s become globally available. |
Bootstrapping the Reactive Fruits application
有了这一点,我们来看看如何使用 Quarkus 开发 CRUD 应用程序,它将使用 I/O 线程来处理 HTTP 请求,与数据库互动,处理结果,并且编写 HTTP 响应;换句话说:反应式 CRUD 应用程序。
With this in mind, let’s see how we can develop a CRUD application with Quarkus, which will use the I/O thread to handle the HTTP requests, interact with a database, process the result, and write the HTTP response; in other words: a reactive CRUD application.
尽管我们建议您遵循分步式说明,但您可以在 [role="bare"]https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-reactive-panache-quickstart中找到最终解决方案。
While we recommend you to follow the step-by-step instructions, you can find the final solution on [role="bare"]https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-reactive-panache-quickstart.
首先,转到 code.quarkus.io并选择以下扩展:
First, go to code.quarkus.io and select the following extensions:
-
Quarkus REST Jackson
-
Hibernate Reactive with Panache
-
Reactive PostgreSQL client
该最后一个扩展是适用于 PostgreSQL 的反应式数据库驱动程序。Hibernate Reactive 使用该驱动程序,与数据库互动时不阻塞呼叫方线程。
The last extension is the reactive database driver for PostgreSQL. Hibernate Reactive uses that driver to interact with the database without blocking the caller thread.
在选中之后,点击“生成您的应用程序”,下载该 zip 文件,解压缩它,并且在您最喜爱的 IDE 中打开代码。
Once selected, click on "Generate your application", download the zip file, unzip it and open the code in your favorite IDE.
Reactive Panache Entity
让我们从 `Fruit`实体开始。使用以下内容创建 `src/main/java/org/acme/hibernate/orm/panache/Fruit.java`文件:
Let’s start with the Fruit
entity.Create the src/main/java/org/acme/hibernate/orm/panache/Fruit.java
file with the following content:
package org.acme.hibernate.orm.panache;
import jakarta.persistence.Cacheable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.reactive.panache.PanacheEntity; (1)
@Entity
@Cacheable
public class Fruit extends PanacheEntity {
@Column(length = 40, unique = true)
public String name;
}
1 | Make sure you import the reactive variant of PanacheEntity . |
该类表示 Fruits
。它是一个具有单字段 (name
) 的简单实体。请注意,它使用 io.quarkus.hibernate.reactive.panache.PanacheEntity
,该变体是 `PanacheEntity`的反应式变体。因此,在幕后,Hibernate 使用我们上面所述的执行模型。它与数据库互动时不阻塞该线程。此外,该反应式 `PanacheEntity`提出了一个反应式 API。我们将使用此 API 来实现 REST 端点。
This class represents Fruits
.
It’s a straightforward entity with a single field (name
).
Note that it uses io.quarkus.hibernate.reactive.panache.PanacheEntity
, the reactive variant of PanacheEntity
.
So, behind the scenes, Hibernate uses the execution model we described above.
It interacts with the database without blocking the thread.
In addition, this reactive PanacheEntity
proposes a reactive API.
We will use this API to implement the REST endpoint.
在更深入地了解之前,打开 `src/main/resources/application.properties`文件并添加:
Before going further, open the src/main/resources/application.properties
file and add:
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
它指示应用程序针对数据库使用 PostgreSQL,并且处理数据库架构生成。
It instructs the application to use PostgreSQL for the database and to handle the database schema generation.
在同一目录中,创建一个 `import.sql`文件,它插入一些水果,所以我们在 dev 模式中不从一个空的数据库开始:
In the same directory, create an import.sql
file, which inserts a few fruits, so we don’t start with an empty database in dev mode:
INSERT INTO fruit(id, name) VALUES (1, 'Cherry');
INSERT INTO fruit(id, name) VALUES (2, 'Apple');
INSERT INTO fruit(id, name) VALUES (3, 'Banana');
ALTER SEQUENCE fruit_seq RESTART WITH 4;
在终端中,使用 `./mvnw quarkus:dev`以 dev 模式启动应用程序。Quarkus 会自动为您启动一个数据库实例并配置应用程序。现在我们只需要实现 HTTP 端点。
In a terminal, launch the application in dev mode using: ./mvnw quarkus:dev
.
Quarkus automatically starts a database instance for you and configure the application. Now we only need to implement the HTTP endpoint.
Reactive Resource
由于与数据库的交互是非阻塞和异步的,我们需要使用异步构造来实现 HTTP 资源。Quarkus 采用 Mutiny 作为其核心响应式编程模型。因此,它支持从 HTTP 端点返回 Mutiny 类型(@“1”和 @“2”)。此外,我们的 Fruit Panache 实体公开了使用这些类型的函数,因此我们只需要实现 @“3”。
Because the interaction with the database is non-blocking and asynchronous, we need to use asynchronous constructs to implement our HTTP resource.
Quarkus uses Mutiny as its central reactive programming model.
So, it supports returning Mutiny types (Uni
and Multi
) from HTTP endpoints.
Also, our Fruit Panache entity exposes methods using these types, so we only need to implement the glue.
使用下列内容创建 @“4” 文件:
Create the src/main/java/org/acme/hibernate/orm/panache/FruitResource.java
file with the following content:
package org.acme.hibernate.orm.panache;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.Path;
@Path("/fruits")
@ApplicationScoped
public class FruitResource {
}
让我们从 @“5” 方法开始。@“6” 方法返回存储在数据库的所有水果。在 @“7” 中,添加下列代码:
Let’s start with the getAll
method. The getAll
method returns all the fruits stored in the database.
In the FruitResource
, add the following code:
@GET
public Uni<List<Fruit>> get() {
return Fruit.listAll(Sort.by("name"));
}
打开 [role="bare"]@“8” 以调用此方法:
Open [role="bare"]http://localhost:8080/fruits to invoke this method:
[{"id":2,"name":"Apple"},{"id":3,"name":"Banana"},{"id":1,"name":"Cherry"},{"id":4,"name":"peach"}]
我们得到了预期的 JSON 数组。除非另有指示,否则 Quarkus REST 会自动将该列表映射到 JSON 数组。
We get the expected JSON array. Quarkus REST automatically maps the list into a JSON Array, except if instructed otherwise.
查看返回类型,它返回一个 @“10” 的 @“9” 。@“11” 是一个异步类型。它有点像 future。它是一个占位符,将在稍后获取其值(项目)。当它接收项目(Mutiny 指出它 @“12” 其项目)时,你可以附加一些行为。这就是我们表达延续的方式:获取一个 uni,当该 uni 发射其项目时,执行处理的其余部分。
Look at the return type; it returns a Uni
of List<Fruit>
.
Uni
is an asynchronous type.
It’s a bit like a future.
It’s a placeholder that will get its value (item) later.
When it receives the item (Mutiny says it emits its item), you can attach some behavior.
That’s how we express the continuation: get a uni, and when the uni emits its item, execute the rest of the processing.
响应式开发人员可能会疑惑为什么我们不能直接返回水果流。在与数据库打交道时,这往往不是一个好主意。关系数据库不能很好地处理流。这是不针对此用例设计的协议的一个问题。因此,要从数据库流式传输行,你需要保持连接(有时还有事务)处于开启状态,直到所有行都被使用。如果你有慢速使用者,你将破坏数据库的黄金法则:不要长时间持有连接。事实上,连接数相当少,而让使用者长时间持有它们,会大大降低应用程序的并发性。因此,尽可能使用 @“13” 并加载内容。如果你有一大组结果,请实现分页。 |
Reactive developers may wonder why we can’t return a stream of fruits directly.
It tends to be a bad idea when dealing with a database.
Relational databases do not handle streaming well.
It’s a problem of protocols not designed for this use case.
So, to stream rows from the database, you need to keep a connection (and sometimes a transaction) open until all the rows are consumed.
If you have slow consumers, you break the golden rule of databases: don’t hold connections for too long.
Indeed, the number of connections is rather low, and having consumers keeping them for too long will dramatically reduce the concurrency of your application.
So, when possible, use a |
让我们使用 @“14” 继续我们的 API:
Let’s continue our API with getSingle
:
@GET
@Path("/{id}")
public Uni<Fruit> getSingle(Long id) {
return Fruit.findById(id);
}
在此情况下,我们使用 @“15” 检索水果。它将返回一个 @“16”,该 @“16” 将在数据库检索行后完成。
In this case, we use Fruit.findById
to retrieve the fruit.
It returns a Uni
, which will complete when the database has retrieved the row.
@“17” 方法允许向数据库添加一个新水果:
The create
method allows adding a new fruit to the database:
@POST
public Uni<RestResponse<Fruit>> create(Fruit fruit) {
return Panache.withTransaction(fruit::persist).replaceWith(RestResponse.status(CREATED, fruit));
}
该代码涉及更多内容。要写入数据库,我们需要一个事务,因此我们使用 @“18” 获取一个(异步)并继续调用 @“19” 方法。@“20” 方法返回一个 @“21”,它发出水果在数据库中插入结果。一旦插入完成(它充当延续的角色),我们创建 @“22” HTTP 响应。
The code is a bit more involved.
To write to a database, we need a transaction, therefore we use Panache.withTransaction
to obtain one (asynchronously) and proceed to call the persist
method.
The persist
method returns a Uni
that emits the result of the insertion of the fruit in the database.
Once the insertion completes (which plays the role of the continuation), we create a 201 CREATED
HTTP response.
如果你的计算机上装有 @“23”,你可以使用以下命令尝试端点:
If you have curl on your machine, you can try the endpoint using:
> curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"peach"}' \
http://localhost:8080/fruits
遵循同样的思路,你可以实现其他 CRUD 方法。
Following the same ideas, you can implement the other CRUD methods.
Testing and Running
测试响应式应用程序类似于测试非响应式应用程序:使用 HTTP 端点并验证 HTTP 响应。应用程序是否响应式这一事实并没有改变任何内容。
Testing a reactive application is similar to testing a non-reactive one: use the HTTP endpoint and verify the HTTP responses. The fact that the application is reactive does not change anything.
在 @“24” 中,你可以查看如何实现水果应用程序的测试。
In FruitsEndpointTest.java you can see how the test for the fruit application can be implemented.
应用程序的打包和运行也不会改变。
Packaging and running the application does not change either.
你可以像往常一样使用以下命令:
You can use the following command as usual:
Unresolved directive in getting-started-reactive.adoc - include::{includes}/devtools/build.adoc[]
或构建一个本机可执行文件:
or to build a native executable:
Unresolved directive in getting-started-reactive.adoc - include::{includes}/devtools/build-native.adoc[]
你也可以将应用程序打包成容器。
You can also package the application in a container.
要运行应用程序,请务必启动一个数据库并向应用程序提供配置。
To run the application, don’t forget to start a database and provide the configuration to your application.
例如,您可以使用 Docker 运行您的数据库:
For example, you can use Docker to run your database:
docker run -it --rm=true \
--name postgres-quarkus -e POSTGRES_USER=quarkus \
-e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=fruits \
-p 5432:5432 postgres:14.1
然后,使用以下内容启动应用程序:
Then, launch the application using:
java \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus \
-jar target/quarkus-app/quarkus-run.jar
或者,如果您将应用程序打包为本地可执行文件,请使用:
Or, if you packaged your application as native executable, use:
./target/getting-started-with-reactive-runner \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus
传递给应用程序的参数已在数据源指南中进行了解释。还有其他方法可用于配置应用程序 - 请查看 configuration guide 以了解各种可能性(例如环境变量、.env 文件等)。
The parameters passed to the application are described in the datasource guide. There are other ways to configure the application - please check the configuration guide to have an overview of the possibilities (such as env variable, .env files and so on).