Иногда возникает необходимость кэширования результатов исполнения методов. Одно из возможных решений для java описано здесь. Всё, в принципе, тривиально: EHCache, Spring AOP для перехвата вызовов, немножко кода.
Рассмотрим, как мне кажется, более элегантное решение на scala.
Сформулируем задачу более конкретно. Требуется создать mixin, при добавлении которого к реализации сервиса, можно будет добавлять кэширование результатов исполнения методов следующим образом:
Начнем с простенького DSL для определения параметров кэширования. Нам потребуется класс для описания параметров кэширования, содержащий:
Определим методы forever, expirationTime, withKey, умолчательное преобразование для функций для заполнения обьектов этого класса, а также метод _execFn, который будет вычислять значение fn, переданное по имени.
Таким образом выражение вида:
Будет возвращать объект CacheOptions[String] с заполненными полями time, key и fn.
Перейдем непосредственно к кэшированию. В статье, которую я упомянул в начале, для кэширования результатов используется EHCache. Ничто не мешает поступить так же, однако, для простоты я покажу кэширование результатов в простом ConcurrentHashMap.
Итак, собственно метод cache:
Никаких космических технологий — попытка выбрать из кэша, проверка timestamp записи, исполнение и кэширование, если это необходимо.
Добавим простенький метод для принудительного исключения записей из кэша:
Готово!
Минус, в сравнении с использованием spring aop — необходимо описывать процедуру создания ключа, в зависимости от входных параметров (в решении на spring aop это происходит автоматически). Плюсы — простота, возможность принудительного удаления записей.
Для примера я показал кэширование в ConcurrentHashMap, минусы такого решения, в сравнении с использованием EHCache, очевидны — нет возможности ограничить количество записей в кэше и определить стратегию удаления записей, необходимо самостоятельно следить за тем чтобы возвращаемые значения были immutable, иначе будет существовать возможность изменить значения, находящиеся в кэше. Но всех этих проблем можно избежать, «переключив» этот trait на использование EHCache или любой другой библиотеки. Это дело 5 минут.
Рассмотрим, как мне кажется, более элегантное решение на 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 минут.