1. 简介

在本教程中,我们将学习如何在 Play Framework 的 Scala 项目中使用其缓存 API。这个 API 虽然看起来非常简单,但能很好地解决我们在开发中经常遇到的问题:避免重复从慢速系统中获取数据,转而使用缓存中的结果

2. 启用缓存支持

要在 Play 应用中使用缓存功能,我们需要先在项目中进行一些配置。

首先,在项目的 build.sbt 文件中添加以下依赖:

libraryDependencies += caffeine

虽然 Play 也支持 Ehcache,但官方目前更推荐使用 Caffeine

添加完依赖后,需要重启 sbt 或重新加载项目。完成后就可以在代码中使用缓存功能了。

3. Play 的 SyncCacheApiAsyncCacheApi

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 接口,获取指定用户最近七天的推文。

使用缓存的原因有两点:

  1. Twitter 对 API 调用频率有限制 ❌
  2. 大多数用户并不会频繁发推 ✅

在示例代码中,我们以 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 获取。


原始标题:Caching in Play Framework for Scala