Caching in a web application is the process of storing dynamically generated items, whether these are data objects, pages, or parts of a page, in memory at the initial time they are requested. This can later be reused if subsequent requests for the same data are made, thereby reducing response time and enhancing user experience. One can cache or store these items on the web server or other software in the request stream, such as the proxy server or browser.
Play has a minimal cache API, which uses EHCache. As stated on its website (http://ehcache.org/):
Ehcache is an open source, standards-based cache for boosting performance, offloading your database, and simplifying scalability. It's the most widely-used Java-based cache because it's robust, proven, and full-featured. Ehcache scales from in-process, with one or more nodes, all the way to mixed in-process/out-of-process configurations with terabyte-sized caches.
It provides caching for presentation layers as well as application-specific objects. It is easy to use, maintain, and extend.
Using the default cache API is similar to using a mutable Map[String, Any]
:
Cache.set("userSession", session) val maybeSession: Option[UserSession] = Cache.getAs[UserSession]("userSession") Cache.remove("userSession")
This API is made available through EHCachePlugin
. The plugin is responsible for creating an instance of EHCache CacheManager with an available configuration on starting the application, and shutting it down when the application is stopped. We will discuss Play plugins in detail in Chapter 13, Writing Play Plugins. Basically, EHCachePlugin
handles all the boilerplate required to use EHCache in an application and EhCacheImpl
provides the methods to do so, such as get
, set
, and remove
. It is defined as follows:
class EhCacheImpl(private val cache: Ehcache) extends CacheAPI { def set(key: String, value: Any, expiration: Int) { val element = new Element(key, value) if (expiration == 0) element.setEternal(true) element.setTimeToLive(expiration) cache.put(element) } def get(key: String): Option[Any] = { Option(cache.get(key)).map(_.getObjectValue) } def remove(key: String) { cache.remove(key) } }
By default, the plugin looks for ehcache.xml
in the conf
directory and, if the file does not exist, the default configuration provided by the ehcache-default.xml
framework is loaded.
It is also possible to specify the location of the ehcache
configuration when starting the application using the ehcache.configResource
argument.
The Cache API also simplifies handling a cache for results from requests on both the client and server side of the application. Adding EXPIRES
and etag
headers can be used to manipulate the client-side cache, while on the server side the results are cached so that its corresponding action is not computed for each call.
For example, we can cache the result of the request used to fetch details of inactive users:
def getInactiveUsers = Cached("inactiveUsers") { Action { val users = User.getAllInactive Ok(Json.toJson(users)) } }
However, what if we want this to get updated every hour? We just need to specify the duration explicitly:
def getInactiveUsers = Cached("inactiveUsers").default(3600) { Action { val users = User.getAllInactive Ok(Json.toJson(users)) } }
All of this is handled by the Cached
case class and its companion object. The case class is defined as follows:
case class Cached(key: RequestHeader => String, caching: PartialFunction[ResponseHeader, Duration]) { … }
The companion object provides commonly required methods to generate cached instances, such as cache action based on its status, and so on.
The apply
method in cached calls the build
method, which is defined as follows:
def build(action: EssentialAction)(implicit app: Application) = EssentialAction { request => val resultKey = key(request) val etagKey = s"$resultKey-etag" // Has the client a version of the resource as fresh as the last one we served? val notModified = for { requestEtag <- request.headers.get(IF_NONE_MATCH) etag <- Cache.getAs[String](etagKey) if requestEtag == "*" || etag == requestEtag } yield Done[Array[Byte], Result](NotModified) notModified.orElse { // Otherwise try to serve the resource from the cache, if it has not yet expired Cache.getAs[Result](resultKey).map(Done[Array[Byte], Result](_)) }.getOrElse { // The resource was not in the cache, we have to run the underlying action val iterateeResult = action(request) // Add cache information to the response, so clients can cache its content iterateeResult.map(handleResult(_, etagKey, resultKey, app)) } }
It simply checks if the result was modified or not. If it hasn't been, it tries to get the result from the Cache
. If the result does not exist in the cache, it fetches it from the action and adds it to the Cache
using the handleResult
method. The handleResult
method is defined as follows:
private def handleResult(result: Result, etagKey: String, resultKey: String, app: Application): Result = { cachingWithEternity.andThen { duration => // Format expiration date according to http standard val expirationDate = http.dateFormat.print(System.currentTimeMillis() + duration.toMillis) // Generate a fresh ETAG for it val etag = expirationDate // Use the expiration date as ETAG val resultWithHeaders = result.withHeaders(ETAG -> etag, EXPIRES -> expirationDate) // Cache the new ETAG of the resource Cache.set(etagKey, etag, duration)(app) // Cache the new Result of the resource Cache.set(resultKey, resultWithHeaders, duration)(app) resultWithHeaders }.applyOrElse(result.header, (_: ResponseHeader) => result) }
If a duration is specified, it returns that else it returns the default duration of one year.
The handleResult
method simply takes the result, adds etag
, expires headers, and then adds the result with the given key to Cache
.
18.117.93.0