Вступление
Мемоизация — это метод кэширования на уровне метода для ускорения последовательных вызовов.
В этом посте будет продемонстрировано, как можно добиться повторяемого чтения на уровне запросов для любого источника данных, используя только Spring AOP .
Весеннее Кеширование
Spring предлагает очень полезную абстракцию кэширования , позволяющую вам отделить логику приложения от деталей реализации кэширования.
Spring Caching использует область действия на уровне приложения, поэтому для запоминания только по запросу мы должны использовать подход DIY .
Кэширование на уровне запросов
Жизненный цикл записи кэша уровня запроса всегда привязан к текущей области запроса. Такой кэш очень похож на Hibernate Persistence Context, который предлагает повторяющиеся чтения на уровне сеанса .
Повторяющиеся операции чтения являются обязательными для предотвращения потери обновлений даже для решений NoSQL.
Пошаговая реализация
Сначала мы определим аннотацию маркера Memoizing:
|
1
2
3
4
|
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Memoize {} |
Эта аннотация собирается явно пометить все методы, которые необходимо запомнить.
Чтобы различать различные вызовы метода, мы собираемся инкапсулировать информацию о вызове метода в следующий тип объекта:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class InvocationContext { public static final String TEMPLATE = "%s.%s(%s)"; private final Class targetClass; private final String targetMethod; private final Object[] args; public InvocationContext(Class targetClass, String targetMethod, Object[] args) { this.targetClass = targetClass; this.targetMethod = targetMethod; this.args = args; } public Class getTargetClass() { return targetClass; } public String getTargetMethod() { return targetMethod; } public Object[] getArgs() { return args; } @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } @Override public String toString() { return String.format(TEMPLATE, targetClass.getName(), targetMethod, Arrays.toString(args)); }} |
Мало кто знает об огромных возможностях бобов Spring Request / Session .
Поскольку нам требуется область запоминания на уровне запроса, мы можем упростить наш дизайн с помощью области запроса Spring, которая скрывает фактическую логику разрешения HttpSession:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@Component@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "request")public class RequestScopeCache { public static final Object NONE = new Object(); private final Map<InvocationContext, Object> cache = new HashMap<InvocationContext, Object>(); public Object get(InvocationContext invocationContext) { return cache.containsKey(invocationContext) ? cache.get(invocationContext) : NONE; } public void put(InvocationContext methodInvocation, Object result) { cache.put(methodInvocation, result); }} |
Так как простая аннотация ничего не значит без обработчика времени выполнения, поэтому мы должны определить Spring Aspect, реализующий реальную логику запоминания:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Aspectpublic class MemoizerAspect { @Autowired private RequestScopeCache requestScopeCache; @Around("@annotation(com.vladmihalcea.cache.Memoize)") public Object memoize(ProceedingJoinPoint pjp) throws Throwable { InvocationContext invocationContext = new InvocationContext( pjp.getSignature().getDeclaringType(), pjp.getSignature().getName(), pjp.getArgs() ); Object result = requestScopeCache.get(invocationContext); if (RequestScopeCache.NONE == result) { result = pjp.proceed(); LOGGER.info("Memoizing result {}, for method invocation: {}", result, invocationContext); requestScopeCache.put(invocationContext, result); } else { LOGGER.info("Using memoized result: {}, for method invocation: {}", result, invocationContext); } return result; }} |
Время тестирования
Давайте проверим все это. Для простоты мы собираемся эмулировать требования запоминания объема на уровне запросов с помощью калькулятора чисел Фибоначчи:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Componentpublic class FibonacciServiceImpl implements FibonacciService { @Autowired private ApplicationContext applicationContext; private FibonacciService fibonacciService; @PostConstruct private void init() { fibonacciService = applicationContext.getBean(FibonacciService.class); } @Memoize public int compute(int i) { LOGGER.info("Calculate fibonacci for number {}", i); if (i == 0 || i == 1) return i; return fibonacciService.compute(i - 2) + fibonacciService.compute(i - 1); }} |
Если мы вычислим 10-е число Фибоначчи, мы получим следующий результат:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
Calculate fibonacci for number 10Calculate fibonacci for number 8Calculate fibonacci for number 6Calculate fibonacci for number 4Calculate fibonacci for number 2Calculate fibonacci for number 0Memoizing result 0, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([0])Calculate fibonacci for number 1Memoizing result 1, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([1])Memoizing result 1, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([2])Calculate fibonacci for number 3Using memoized result: 1, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([1])Using memoized result: 1, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([2])Memoizing result 2, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([3])Memoizing result 3, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([4])Calculate fibonacci for number 5Using memoized result: 2, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([3])Using memoized result: 3, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([4])Memoizing result 5, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([5])Memoizing result 8, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([6])Calculate fibonacci for number 7Using memoized result: 5, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([5])Using memoized result: 8, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([6])Memoizing result 13, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([7])Memoizing result 21, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([8])Calculate fibonacci for number 9Using memoized result: 13, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([7])Using memoized result: 21, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([8])Memoizing result 34, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([9])Memoizing result 55, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([10]) |
Вывод
Мемоизация является сквозной задачей, и Spring AOP позволяет вам отделить детали кэширования от фактического логического кода приложения.
- Код доступен на GitHub .
| Ссылка: | Весеннее напоминание на уровне запроса от нашего партнера JCG Влада Михалча в блоге Влада Михалча . |