响应式入门
Reactive 是一组用于构建健壮、高效且并发的应用程序和系统的原则。 这些原则使您可以处理比传统方法更多的负载,同时更有效地使用资源(CPU 和内存),同时还可以优雅地对故障做出反应。
Quarkus是一个 Reactive 框架。 从一开始,Reactive 就一直是Quarkus架构的基本原则。 它包括许多反应式功能,并提供广泛的生态系统。
本指南不是一篇关于 Reactive 是什么以及Quarkus如何实现响应式架构的深入文章。 如果你想阅读更多关于这些主题的信息,请参考 响应式架构指南,它提供了Quarkus响应式生态系统的概述。
在本指南中,我们将带您开始了解Quarkus的一些响应式功能。 我们将实现一个简单的 CRUD 应用程序。 这部分与 Hibernate与Panache指南不同,它使用了Quarkus的响应式功能。
本指南将帮助您:
-
使用 Quarkus 引导响应式 CRUD 应用程序
-
将 Hibernate Reactive 与 Panache 结合使用,以响应式方式与数据库进行交互
-
Using Quarkus REST (formerly RESTEasy Reactive) to implement HTTP API while enforcing the reactive principle
-
打包和运行应用程序
先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
验证Maven正在使用您期望的Java版本。如果安装了多个JDK,请确保Maven使用的是适当的版本。你可以通过运行 mvn --version 来验证Maven使用的是哪个JDK
|
指令式与响应式:线程的问题
如上所述,在本指南中,我们将实现一个响应式CRUD应用程序。但您可能想知道,与传统的、指令式的模式相比,它有什么不同和好处。
为了更好地理解这种对比,我们需要解释响应式执行模型和指令式执行模型之间的区别。理解 Reactive 不仅仅是一种不同的执行模型是必要的,而且这一区别对于理解本指南是必要的。
在传统的指令式方法中,框架分配一个线程来处理请求。因此,请求的整个处理都运行在这个工作线程上。这个模型的扩展性不太好。事实上,要处理多个并发请求,你需要多个线程。因此,应用程序的并发性受到线程数量的限制。此外,只要您的代码与远程服务交互,这些线程就会被阻塞。因此,这会导致资源的低效使用,因为您可能需要更多的线程,而每个线程在映射到OS线程时,在内存和CPU方面都有成本。
另一方面,响应式模型依赖于非阻塞 I/O和不同的执行模型。非阻塞I/O提供了一种处理并发I/O的有效方法。最小数量的线程称为I/O线程,可以处理许多并发I/O。使用这样的模型,请求处理不会委托给工作线程,而是直接使用这些I/O线程。它节省了内存和CPU,因为不需要创建工作线程来处理请求。它还改善了并发性,因为它消除了对线程数量的限制。最后,它还改善了响应时间,因为它减少了线程开关的数量。
从顺序到延续风格
因此,使用响应式执行模型,请求是使用I/O线程处理的。但这还不是全部。一个I/O线程可以处理多个并发请求。如何实现?这是一个技巧,也是响应式和指令式之间最重要的区别之一。
当处理请求需要与远程服务(如HTTP API或数据库)交互时,它不会在等待响应时阻塞执行。相反,它调度I/O操作并附加一个延续,即请求处理剩余的代码。这种延续可以作为回调传递 (与I/O结果一起调用的函数),或者使用更高级的结构,诸如响应式编程或协同程序。不管延续是如何表示的,最基本的方面是释放I/O线程,因此,这个线程可以用来处理另一个请求。当计划的I/O完成时,I/O线程执行延续,并继续处理挂起的请求。
因此,与I/O阻塞执行的指令式模型不同,响应式切换到基于延续的设计,其中I/O线程被释放,而延续在I/O完成时被调用。因此,I/O线程可以处理多个并发请求,从而提高应用程序的整体并发性。
但是,有一个问题。我们需要一种方法来编写延续传递代码。有很多方法可以做到这一点。在Quarkus,我们提出:
-
Mutiny - 一个直观的事件驱动的响应式编程库
-
Kotlin co-routines - 一种以顺序方式编写异步代码的方法
在本指南中,我们将使用Mutiny。想要了解更多关于Mutiny的信息,请查看 Mutiny文档。
Loom项目很快就会加入到JDK中,它提出了一个基于虚拟线程的模型。一旦在全局范围内可用,Quarkus架构就可以支持Loom。 |
启动响应式水果应用程序
考虑到这一点,让我们看看如何使用Quarkus开发一个CRUD应用程序,它将使用I/O线程处理HTTP请求,与数据库交互,处理结果,并编写HTTP响应,换句话说:一个响应式CRUD应用程序。
虽然我们建议您按照步骤操作,但您可以在 https://github.com/quarkusio/quarkus-quickstarts/tree/main/hibernate-reactive-panache-quickstart上找到最终的解决方案。
首先,访问 code.quarkus.io 并选择以下扩展:
-
Quarkus REST Jackson
-
Hibernate Reactive with Panache
-
Reactive PostgreSQL client
最后一个扩展是PostgreSQL的响应式数据库驱动程序。Hibernate响应式使用该驱动程序与数据库交互,而不会阻塞调用线程。
选中后,单击 "Generate your application",下载压缩文件,解压缩并在您喜欢的IDE中打开代码。
响应式Panache实体
让我们从 Fruit
实体开始。创建 src/main/java/org/acme/hibernate/orm/panache/Fruit.java
文件,内容如下:
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 | 确保你导入了 PanacheEntity 的响应式类库。 |
这个类代表 Fruits
。它是一个简单的实体,只有一个字段 (name
)。注意,它使用的是 io.quarkus.hibernate.reactive.panache.PanacheEntity
,是 PanacheEntity
的响应式类库。因此,Hibernate在幕后使用我们前面描述的执行模型。它与数据库交互而不阻塞线程。此外,这个响应式的 PanacheEntity
提出了一个响应式API。我们将使用这个API来实现REST端点。
在进行下一步之前,请打开 src/main/resource/application.properties
文件并添加下面这一段内容到该文件:
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
它指示应用程序使用PostgreSQL数据库,并处理数据库模式的生成。
在同一个目录中,创建一个 import.sql
文件,它插入了一些水果数据,所以我们不会在dev模式中面对一个空数据库开始:
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
。Quarkus会自动为您启动一个数据库实例并配置应用程序。现在我们只需要实现HTTP端点。
响应式资源
由于与数据库的交互是非阻塞的和异步的,我们需要使用异步构造来实现我们的HTTP资源。Quarkus使用Mutiny作为其核心响应式编程模型。因此,它支持从HTTP端点返回Mutiny类型 (Uni
and Multi
) 。此外,水果Panache实例使用这些类型公开方法,因此我们只需要实现 glue。
创建 src/main/java/org/acme/hibernate/orm/panache/FruitResource.java
文件,内容如下:
package org.acme.hibernate.orm.panache;
import java.util.List;
import io.quarkus.panache.common.Sort;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.Path;
@Path("/fruits")
@ApplicationScoped
public class FruitResource {
}
让我们从 getAll
方法开始。 getAll
方法返回存储在数据库中的所有水果对象。在 FruitResource
中,添加以下代码:
@GET
public Uni<List<Fruit>> get() {
return Fruit.listAll(Sort.by("name"));
}
打开 http://localhost:8080/fruits 调用这个方法:
[{"id":2,"name":"Apple"},{"id":3,"name":"Banana"},{"id":1,"name":"Cherry"},{"id":4,"name":"peach"}]
We get the expected JSON array. Quarkus REST automatically maps the list into a JSON Array, except if instructed otherwise.
看看返回类型,它返回 List<Fruit>
的 Uni
。Uni
是一个异步类型。这有点像未来。它是一个占位符,稍后将获得它的值条目 (item) 。当它接收条目(Mutiny说它 输出 的条目)时,你可以附加一些行为。这就是我们表示延续的方式:获取一个uni,当uni输出它的条目时,执行其余的处理。
响应式程序开发者可能会想为什么我们不能直接返回水果流。在处理数据库时,这往往不是一个好主意。关系数据库不能很好地处理流。这是协议不是为这个用例设计的问题。因此,要从数据库流输出行,您需要保持一个连接(有时是一个事务)打开,直到所有行都被消耗掉。如果您的用户很慢,那么您就违反了数据库的黄金法则:不要保持连接太长时间。实际上,可用连接的数量相当少,并且让使用者保持这些连接太长时间将极大地降低应用程序的并发性。所以,如果可能的话,使用 Uni<List<T>> 并加载内容。如果有大量结果,则实现分页。
|
让我们用 getSingle
继续我们的API:
@GET
@Path("/{id}")
public Uni<Fruit> getSingle(Long id) {
return Fruit.findById(id);
}
在本例中,我们使用 Fruit.findById
来检索水果。当数据库检索到该行时,它将返回一个 Uni
。
create
方法允许向数据库中添加新水果记录:
@POST
public Uni<RestResponse<Fruit>> create(Fruit fruit) {
return Panache.withTransaction(fruit::persist).replaceWith(RestResponse.status(CREATED, fruit));
}
这代码要更复杂一些。想要写入数据库,我们需要一个事务。因此我们使用 Panache.withTransaction
来获取一个(异步),然后调用 persist
方法。 persist
方法返回一个 发出`fruit`插入到数据库中结果的`Uni`。一旦插入完成(它起到延续的作用),我们就会创建一个 201 CREATED
的HTTP响应。RESTEasy Reactive会自动将请求体读取为JSON并创建 Fruit
实例。
如果在您的机器安装了 curl命令,便可以尝试使用端点:
> curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"peach"}' \
http://localhost:8080/fruits
遵循相同的思路,您可以实现其他CRUD方法。
测试和运行
测试响应式应用程序与测试非响应式应用程序类似:使用HTTP端点并验证HTTP响应。应用程序是响应式的这一事实并没有改变任何事情。
在 FruitsEndpointTest.java中,您可以看到如何实现水果应用程序的测试。
打包和运行应用程序也不会改变。
您可以像往常一样使用以下命令:
quarkus build
./mvnw install
./gradlew build
或者构建一个原生可执行文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
还可以将应用程序打包在容器中。
要运行应用程序,不要忘记启动数据库并为应用程序提供配置。
例如,你可以使用Docker来运行你的数据库:
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
然后,使用以下命令启动应用程序:
java \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus \
-jar target/quarkus-app/quarkus-run.jar
或者,如果你将你的应用打包为原生可执行文件,使用:
./target/getting-started-with-reactive-runner \
-Dquarkus.datasource.reactive.url=postgresql://localhost/fruits \
-Dquarkus.datasource.username=quarkus \
-Dquarkus.datasource.password=quarkus
数据源指南中描述了这些传递给应用程序的参数。还有其他方法可以配置应用程序——请查看 配置指南来了解各种可能性(例如`env`变量,`.env`文件等)。