Pull to refresh

Scala: Кэширование результатов исполнения методов

Reading time 4 min
Views 2.5K
Иногда возникает необходимость кэширования результатов исполнения методов. Одно из возможных решений для java описано здесь. Всё, в принципе, тривиально: EHCache, Spring AOP для перехвата вызовов, немножко кода.

Рассмотрим, как мне кажется, более элегантное решение на scala.

Сформулируем задачу более конкретно. Требуется создать mixin, при добавлении которого к реализации сервиса, можно будет добавлять кэширование результатов исполнения методов следующим образом:

    final val CACHE_FIRST_USER_CREATED = "firstUserCreated"<br>
    def isFirstUserCreated:Boolean = cache (<br>
      {userDAO.getCount>0} withKey CACHE_FIRST_USER_CREATED forever<br>
    )<br>


Начнем с простенького DSL для определения параметров кэширования. Нам потребуется класс для описания параметров кэширования, содержащий:
  • ключ,
  • время жизни записи,
  • собственно функция вычисляющая возвращаемое значение.

  class CacheOptions[T](fn: =>T) {<br>
    var time = -1<br>
    var key:String = "key"<br>
  }<br>

Определим методы forever, expirationTime, withKey, умолчательное преобразование для функций для заполнения обьектов этого класса, а также метод _execFn, который будет вычислять значение fn, переданное по имени.

  class CacheOptions[T](fn: =>T) {<br>
    var time = -1<br>
    var key:String = "key"<br>
    def _execFn:T = fn<br>
    def forever = {this.time= -1;this}<br>
    def expirationTime(time:Int) = {this.time=time;this}<br>
    def withKey(key:String) = {this.key=key;this}<br>
  }<br>
<br>
  implicit def fn2co[T](fn: =>T) = new CacheOptions[T](fn)<br>
<br>

Таким образом выражение вида:
{ "lazy value" } withKey "mykey" expirationTime 30000<br>

Будет возвращать объект CacheOptions[String] с заполненными полями time, key и fn.

Перейдем непосредственно к кэшированию. В статье, которую я упомянул в начале, для кэширования результатов используется EHCache. Ничто не мешает поступить так же, однако, для простоты я покажу кэширование результатов в простом ConcurrentHashMap.

Итак, собственно метод cache:
  private val cache = new ConcurrentHashMap[String,CacheRecord]<br>
<br>
  def cache[T](co:CacheOptions[T]):T = {<br>
    val timestamp = System.currentTimeMillis()<br>
    val cr = cache.get(co.key)<br>
    (cr==null) match {<br>
      case true => refresh(timestamp,co)<br>
      case false => <br>
        if (co.time>0 && (timestamp-cr.timestamp)>co.time) <br>
          refresh(timestamp,co) <br>
        else<br>
          cr.obj.asInstanceOf[T]<br>
    }<br>
  }<br>
<br>
  private def refresh[T](timestamp:Long, co:CacheOptions[T]):T = {<br>
    val cr = new CacheRecord(timestamp, co._execFn.asInstanceOf[AnyRef])<br>
    cache.put(co.key, cr)<br>
    cr.obj.asInstanceOf[T]<br>
  }<br>
<br>

Никаких космических технологий — попытка выбрать из кэша, проверка timestamp записи, исполнение и кэширование, если это необходимо.

Добавим простенький метод для принудительного исключения записей из кэша:
  def evict(key:String):Unit = cache.remove(key)<br>

Готово!

Минус, в сравнении с использованием spring aop — необходимо описывать процедуру создания ключа, в зависимости от входных параметров (в решении на spring aop это происходит автоматически). Плюсы — простота, возможность принудительного удаления записей.

Для примера я показал кэширование в ConcurrentHashMap, минусы такого решения, в сравнении с использованием EHCache, очевидны — нет возможности ограничить количество записей в кэше и определить стратегию удаления записей, необходимо самостоятельно следить за тем чтобы возвращаемые значения были immutable, иначе будет существовать возможность изменить значения, находящиеся в кэше. Но всех этих проблем можно избежать, «переключив» этот trait на использование EHCache или любой другой библиотеки. Это дело 5 минут.
Tags:
Hubs:
+2
Comments 2
Comments Comments 2

Articles