1. 简介
在本教程中,我们将学习如何在 Play Framework 的 Scala 项目中使用其缓存 API。这个 API 虽然看起来非常简单,但能很好地解决我们在开发中经常遇到的问题:避免重复从慢速系统中获取数据,转而使用缓存中的结果。
2. 启用缓存支持
要在 Play 应用中使用缓存功能,我们需要先在项目中进行一些配置。
首先,在项目的 build.sbt 文件中添加以下依赖:
libraryDependencies += caffeine
虽然 Play 也支持 Ehcache,但官方目前更推荐使用 Caffeine。
添加完依赖后,需要重启 sbt 或重新加载项目。完成后就可以在代码中使用缓存功能了。
3. Play 的 SyncCacheApi 和 AsyncCacheApi
Play 提供了两种缓存 API:同步和异步。
对于 Web 应用来说,我们更推荐使用异步版本,因为它更契合 Web 服务的非阻塞特性。
以下是 AsyncCacheApi 提供的主要方法:
set(key: String, value: Any, expiry: Duration = Duration.Inf): Future[Done]
✅
将值写入缓存,并设置过期时间(默认永不过期)remove(key: String): Future[Done]
✅
删除指定 key 的缓存项get[T](key: String): Future[Option[T]]
✅
获取指定 key 的缓存值(如果存在)getOrElseUpdate[T](key: String, expiry: Duration)(orElse: => Future[T]): Future[T]
✅
如果缓存中有值则返回,否则执行orElse
方法,并将结果缓存起来removeAll(): Future[Done]
✅
清空整个缓存
⚠️ 注意:同步版本的 SyncCacheApi 不支持 removeAll()
方法。
接下来我们将重点介绍 getOrElseUpdate()
方法的使用。
4. 使用缓存 API
为了演示缓存的使用,我们构建一个简单的应用,调用 Twitter 的 recent search 接口,获取指定用户最近七天的推文。
使用缓存的原因有两点:
- Twitter 对 API 调用频率有限制 ❌
- 大多数用户并不会频繁发推 ✅
在示例代码中,我们以 Twitter 用户名为缓存的 key,并设置缓存项在 5 分钟后过期。这个时间可以通过 application.conf 文件中的 twitterCache.expiry
配置项来调整。
4.1. Web 控制器
我们的控制器类是继承自 Play 的 BaseController,名为 TwitterController:
@Singleton
class TwitterController @Inject()(
twitterSearchService: TwitterSearchService,
override val controllerComponents: ControllerComponents,
implicit val executionContext: ExecutionContext
) extends BaseController {
def recentSearch(twitterAccount: String): Action[AnyContent] = Action.async {
twitterSearchService.recentSearch(twitterAccount).map { response =>
Ok(Json.toJson(response))
}
}
}
在 Play 的 routes 文件中配置接口路径:
GET /api/twitter/recentSearch/:twitterAccount controllers.TwitterController.recentSearch(twitterAccount)
启动应用后,可以通过 curl 调用接口:
curl http://localhost:9000/api/twitter/recentSearch/TwitterDev | jq
接下来我们看看服务层的实现,这是使用缓存的关键部分。
4.2. 搜索服务层
服务层代码非常简洁,也是我们使用 Play 缓存 API 的地方:
class TwitterSearchService @Inject()(twitterWebApi: TwitterWebApi,
cache: AsyncCacheApi,
configuration: Configuration,
implicit val executionContext: ExecutionContext) {
val cacheExpiry: Duration = configuration.get[Duration]("twitterCache.expiry")
def recentSearch(twitterUser: String): Future[Map[String, JsValue]] = {
cache.getOrElseUpdate[JsValue](twitterUser, cacheExpiry) {
twitterWebApi.recentSearch(twitterUser)
}.map(_.as[Map[String, JsValue]])
}
}
如前所述,getOrElseUpdate()
方法会先尝试从缓存中获取数据,如果不存在则调用 Twitter API 获取最新数据并缓存起来。这是缓存使用中最常见的模式,也适用于大多数场景。
4.3. API 客户端
用于调用 Twitter API 的客户端代码如下,它不包含任何缓存逻辑:
def recentSearch(fromTwitterUser: String): Future[JsValue] = {
val url = String.format(recentSearchUrl, fromTwitterUser)
wsClient
.url(url)
.withHttpHeaders(
HeaderNames.ACCEPT -> MimeTypes.JSON,
HeaderNames.AUTHORIZATION -> s"Bearer $bearerToken"
).get()
.map { response =>
if (response.status == OK) {
response.json
} else {
throw ApiError(response.status, Some(response.statusText))
}
}
}
⚠️ 注意:Twitter API 需要通过 Bearer Token 认证,你需要先注册一个 Twitter 开发者账号才能获取。另外,Play 的 WSRequest.withAuth()
方法只支持用户名密码认证(如 Basic Auth),所以这里我们手动设置了 Header。
4.4. 缓存 API 的注意事项
⚠️ 需要注意的是,无论是 SyncCacheApi 还是 AsyncCacheApi,它们都不提供线程同步机制。也就是说,对于同一个 key,可能会出现多个并发请求同时触发 API 调用的情况。
此外,我们使用的 Caffeine 缓存并不是分布式缓存。如果你的应用部署了多个实例,每个实例都会拥有独立的缓存。如果需要共享缓存,可以考虑使用 Ehcache 并进行分布式配置。
5. 总结
本文介绍了 Play Framework 提供的缓存 API,并演示了如何在实际项目中使用它。可以看到,Play 让缓存功能的集成变得非常简单。
✅ 完整源码可从 GitHub 获取。