在 Vert.x 应用中使用 Caffeine 的 AsyncLoadingCache
本文档将向你展示如何在 Vert.x 应用程序中使用 Caffeine 提供的 AsyncLoadingCache 异步加载缓存。
你将构建什么
你将构建一个在网页中轮播切换猫咪图片的应用程序。
图片将由服务端从公开 API https://http.cat 下载。
每张图片代表一个以 🐱 形象呈现的 HTTP 状态码。
准备好了吗?
| 图片资源由 Tomomi Imura (@girlie_mac) 创作。 |
前置要求
-
文本编辑器或 IDE
-
Java 11 或更高版本
-
Maven 或 Gradle
创建工程项目
本项目代码同时提供了功能完全等价的 Maven 与 Gradle 构建配置。
使用 Maven
这是你应当使用的 pom.xml 文件内容:
pom.xml<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.vertx.howtos</groupId>
<artifactId>async-loading-cache-caffeine-howto</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<vertx.version>5.0.0.CR2</vertx.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>${vertx.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>io.vertx.howtos.caffeine.CatsVerticle</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
使用 Gradle
假设你使用基于 Kotlin DSL 的 Gradle 构建,这是你的 build.gradle.kts 文件内容:
build.gradle.ktsplugins {
java
application
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
dependencies {
implementation(platform("io.vertx:vertx-stack-depchain:5.0.0.CR2"))
implementation("io.vertx:vertx-core")
implementation("io.vertx:vertx-web")
implementation("io.vertx:vertx-web-client")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.5")
}
application {
mainClass = "io.vertx.howtos.caffeine.CatsVerticle"
}
网页端实现
index.html 网页主体主要由以下部分构成:
-
页面 body 中的一个
<img>标签; -
一个在页面加载完成后动态更改图片
src属性的脚本。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rotating Cats Page</title>
<script type="application/javascript">
window.onload = function () { (2)
const codes = [ (3)
100,
200, 201, 204, 206,
301, 302, 303, 304, 307, 308,
401, 403, 404, 406, 407, 409, 410, 412, 416, 418, 425, 451,
500, 501, 502, 503, 504,
]
setInterval(function () { (4)
let img = document.getElementById("cat-img"); (5)
img.src = "/api/cats/" + codes[Math.floor(Math.random() * codes.length)]; (6)
}, 250)
}
</script>
</head>
<body>
<div>
<img src="/api/cats/200" id="cat-img" alt="Cat image"/> (1)
</div>
</body>
</html>
| 1 | body 中的 img 标签,id 设置为 cat-img |
| 2 | 当页面加载完成时触发执行脚本函数 |
| 3 | 定义一组候选 HTTP 状态码 |
| 4 | 安排一个以 250 毫秒固定间隔周期性执行的调度函数 |
| 5 | 通过元素 id 从 DOM 中获取 img 元素 |
| 6 | 随机选取一个 HTTP 状态码并更新 src 属性 |
服务端实现
我们需要一个 Vert.x Web Client 实例来远程获取图片:
request = WebClient.create(vertx)
.request(GET, new RequestOptions().setHost("http.cat").setPort(443).setSsl(true))
.as(BodyCodec.buffer());
我们还需要一个缓存,以避免对后端 API 造成过载:
cache = Caffeine.newBuilder() (1)
.expireAfterWrite(Duration.ofMinutes(1)) (2)
.recordStats() (3)
.executor(cmd -> context.runOnContext(v -> cmd.run())) (4)
.buildAsync((key, exec) -> CompletableFuture.supplyAsync(() -> { (5)
Future<Buffer> future = fetchCatImage(key); (6)
return future.toCompletionStage(); (7)
}, exec).thenComposeAsync(Function.identity(), exec));
vertx.setPeriodic(20000, l -> { (8)
CacheStats stats = cache.synchronous().stats();
log.info("Stats: " + stats);
});
| 1 | 创建缓存构建器 Builder |
| 2 | 配置缓存条目在写入 1 分钟后过期 |
| 3 | 启用统计信息指标收集 |
| 4 | 定义一个在当前 Verticle 上下文中触发执行任务的调度器 Executor |
| 5 | 利用缓存调度器创建异步加载器,该加载器必须返回一个 CompletableFuture |
| 6 | 远程拉取猫咪图片 |
| 7 | 将 Vert.x 的 Future 转换为 Java 标准的 CompletionStage |
| 8 | 周期性打印缓存统计信息 |
|
在这里,自定义执行器(Executor)定义与相对复杂的加载器实现并非绝对必需。 事实上,当我们仅部署单个 Verticle 实例、将缓存绑定到私有成员字段、且始终在 Event Loop 事件循环中调用缓存方法时,你可以将其极简化为:
然而,如果你计划部署多个 Verticle 实例并在它们之间共享该缓存,请坚持使用前面的完整实现。它能确保异步加载器始终在正确的 Verticle 执行上下文中被调用。 |
获取猫咪图片的具体逻辑是:将对应的 HTTP 状态码作为请求 URI 发送给后端:
private Future<Buffer> fetchCatImage(int code) {
return request.uri("/" + code)
.send()
.expecting(HttpResponseExpectation.SC_OK)
.map(HttpResponse::body);
}
借助 Vert.x Web,为我们的 API 以及静态资源文件搭建 HTTP 服务器非常直观:
Router router = Router.router(vertx);
router.get("/api/cats/:id").produces("image/*").handler(this::handleImageRequest);
router.get().handler(StaticHandler.create());
return vertx.createHttpServer()
.requestHandler(router)
.listen(8080)
.onSuccess(v -> log.info("Server started on port 8080"));
这是处理图片请求的具体实现方式:
private void handleImageRequest(RoutingContext rc) {
Integer code = Integer.valueOf(rc.pathParam("id")); (1)
CompletableFuture<Buffer> completableFuture = cache.get(code); (2)
Future<Buffer> future = Future.fromCompletionStage(completableFuture, context); (3)
future.onComplete(ar -> { (4)
if (ar.succeeded()) {
rc.response()
.putHeader("Cache-Control", "no-store") (5)
.end(ar.result());
} else {
rc.fail(ar.cause());
}
});
}
| 1 | 从请求路径中提取指定的状态码 |
| 2 | 调用 Caffeine 缓存(如果缓存未命中,将透明地从后端加载图片) |
| 3 | 将 Caffeine 返回的 CompletableFuture 转换回 Vert.x 的 Future |
| 4 | 异步完成后,将图片二进制字节(或错误信息)响应给客户端 |
| 5 | 通知浏览器禁用对该图片的本地缓存(否则浏览器针对同一状态码只会向服务端发起一次真实请求!) |
运行应用程序
CatsVerticle 需要一个 main 入口方法:
public static void main(String[] args) {
Vertx vertx = Vertx.vertx(); (1)
vertx.deployVerticle(new CatsVerticle()).await(); (2)
}
| 1 | 创建 Vert.x 实例 |
| 2 | 部署 CatsVerticle |
你可以通过以下方式启动应用程序:
-
在你的 IDE 中直接运行
CatsVerticle类中的main方法; -
或通过 Maven 运行:
mvn compile exec:java; -
或通过 Gradle 运行:
./gradlew run(Linux, macOS)或gradlew run(Windows)。
在浏览器中打开 http://localhost:8080。
你将看到页面中猫咪图片在快速轮播切换:
运行一段时间后,查看控制台输出。 你应该会看到类似以下的日志:
Mar 22, 2022 3:45:17 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=52, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:45:37 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=132, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:45:57 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=212, missCount=28, loadSuccessCount=28, loadFailureCount=0, totalLoadTime=2514949257, evictionCount=0, evictionWeight=0}
Mar 22, 2022 3:46:17 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=267, missCount=53, loadSuccessCount=52, loadFailureCount=0, totalLoadTime=3337599348, evictionCount=28, evictionWeight=28}
Mar 22, 2022 3:46:37 PM io.vertx.howtos.caffeine.CatsVerticle lambda$start$4
INFO: Stats: CacheStats{hitCount=344, missCount=56, loadSuccessCount=56, loadFailureCount=0, totalLoadTime=3480880213, evictionCount=28, evictionWeight=28}
观察 hitCount(命中数)、missCount(未命中数)和 evictionCount(驱逐数)随时间的动态变化。
总结
本文档涵盖了以下内容:
-
使用 Vert.x Web Client 发起异步 HTTP 请求;
-
使用 Vert.x Web 路由器与 HTTP 服务端;
-
在 Vert.x 应用中整合 Caffeine 异步加载缓存(AsyncLoadingCache);
-
Vert.x 定时周期任务的使用。