全面拥抱JVM 25虚拟线程
该项目 AngularAndSpring 已从 Spring Reactor 重构为使用 JDK 25 和虚拟线程的 Spring 阻塞式 API。
旧架构
AngularAndSpring 之所以使用 Spring Reactor 构建,主要是为了在高负载下高效利用操作系统线程和内存。Spring Reactor 使用有限的线程池(通常等于可用 CPU 核心数),仅在 CPU 有工作要做时才使用 OS 线程。它不会让操作系统线程休眠。这样可以更好地利用 CPU 资源,并且需要的 OS 线程少得多,而每个 OS 线程大约需要 1 MB 内存。
代价是响应式编程风格,随着所实现功能的复杂度增长,代码会变得更难阅读,同时堆栈跟踪也非常难以阅读。这些难以阅读的堆栈跟踪使查找和修复错误变得更加困难,并且显著更耗时。
虚拟线程如何改变权衡
在 JDK 25 之前,选择在于:使用 Spring Reactor 获得高效的硬件利用,还是使用易于阅读、调试更简单、堆栈跟踪可读的代码。Spring 对 Java 虚拟线程 的支持提供了使用非常高效的虚拟线程来处理请求。使用虚拟线程时,只有在需要处理时才使用操作系统线程;虚拟线程会从 OS 线程上卸载,从而释放 OS 线程去运行另一个虚拟线程;并且虚拟线程的内存需求极小。这意味着有限数量的操作系统线程能够处理非常大量的虚拟线程。
随着 JDK 25 和虚拟线程更新,这种选择已经改变:使用虚拟线程的 JDK 25 可以高效利用硬件资源,同时你可以保持代码易于阅读和调试,并拥有可读的堆栈跟踪。当调用原生代码时,虚拟线程存在一些限制,但 Spring Reactor 也同样存在这些限制。虚拟线程的主要障碍:缺失的 synchronized 支持已在 JDK 25 中修复。这意味着 Spring Reactor 的主要优势现在由 JVM 及其虚拟线程提供。
新架构
新架构的基本构建块可以在后端 pom.xml 中找到。它现在使用这些 starter:
虚拟线程通过 application.properties 中的以下配置开启:
spring.threads.virtual.enabled=true
spring-boot-starter-webmvc 用于提供带阻塞式 API 的 REST 端点。spring-boot-starter-restclient 用于提供新的 REST 客户端,以调用其他系统的阻塞式 REST 端点。spring-boot-starter-data-mongodb 用于提供访问 MongoDB 的 repository 和 API;所有这些 API 都是阻塞式的。spring.threads.virtual.enabled=true 启用了 Spring Boot 对 JVM 提供的虚拟线程的支持。
REST 端点
阻塞式 REST 端点现在在 BitstampController 中如下所示:
这些端点不再返回 reactor 包装类型。它们只返回预期的返回类型。BitstampService 方法现在也返回预期的返回类型,不带任何包装器。这些端点只使用普通 Java。
RestClient
新的阻塞式 RestClient 现在就是 Spring RestClient,它在 ScheduledTask 中如下所示:
RestClient 使用 RestClientBuilder 创建,以便配置连接和读取超时。超时在 customRequestFactory() 方法中创建,并作为 RestClientBuilder 的参数设置。RestClient 使用 .uri(...) 方法设置要调用的 URL。.accept(...) 方法设置 REST 客户端期望从 HTTP 头中接收的 MediaType。.retrieve() 方法执行 REST 调用,.body(...) 方法指定 JSON 响应应映射到的对象。为了调试,先将响应体映射为 String 并分析响应,以确保它能正确映射到相应的 DTO,这可能会很有用。
MongoDB 客户端
为了访问数据库,使用了 Spring Data MongoDB。每分钟报价位于 QuoteBs、QuoteBf、QuoteCb 实体中。每小时报价位于 QuoteHourBs、QuoteHourBf、QuoteHourCb 实体中,每日报价位于 QuoteDayBs、QuoteDayBf、QuoteDayCb 实体中。不同的实体使 Spring Data MongoDB 能够为每个实体提供自己的 repository。这是 QuoteBsMongoRepository 的一个示例:
接口 MongoRepository 提供标准接口,并在 QuoteBsRepository 中启用生成的 CRUD 方法。
此接口使用 Spring Data MongoDB 提供的按方法名生成的 CRUD 方法,并对无法仅通过方法名生成的查询方法使用 @Query 注解。实体 QuoteBs 如下所示:
@Document 注解将类标记为 MongoDB 文档。QuotePairBs 接口用于访问同一来源的分钟、小时和日实体的公共属性。@Indexed 注解确保在 MongoDB 中为此属性创建索引,以便更快地访问和排序。Java BigDecimal 类型的精度高于 MongoDB number 类型的精度。因此,ScheduledTask 中使用了此方法:
所有 BigDecimal 类型属性的精度限制为 30 位小数。对于此应用程序中的所有用例来说,这已经足够。
线程池
如果要从大量分钟报价重新计算小时和日报价,这可能需要几分钟。为了避免与处理用户请求的线程池竞争,在 SchedulingConfig 中为此任务创建了一个单独的线程池:
这会创建一个单独的 TaskExecutor,可以在 @Async("clientTaskExecutor") 注解中使用。这样可以确保当长时间运行的线程正在工作时,不会有用户请求在队列中等待空闲线程。
结论
借助 JVM 25 中的虚拟线程以及 Spring 对它们的支持,在 Spring 中使用响应式编程的许多理由已经消失。虚拟线程的 synchronized 问题已在 JDK 25 中修复,而其余导致线程固定的原因对大多数用例来说已不再相关。JVM 现在开箱即用地提供了响应式框架的可扩展性和效率,而阻塞式编程模型更易于实现和调试。Spring Data MongoDB 支持通过生成的 CRUD 方法和 @Query(...) 支持,使数据库访问变得更加容易。对于仍在使用响应式编程的项目来说,是时候迁移到对开发者来说更简单、更快速的编程模型了。DZone 贡献者表达的观点仅代表他们自己。