Вступление
Мемоизация — это метод кэширования на уровне метода для ускорения последовательных вызовов.
В этом посте будет продемонстрировано, как можно добиться повторяемого чтения на уровне запросов для любого источника данных, используя только 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
|
@Aspect public 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
|
@Component public 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 10 Calculate fibonacci for number 8 Calculate fibonacci for number 6 Calculate fibonacci for number 4 Calculate fibonacci for number 2 Calculate fibonacci for number 0 Memoizing result 0, for method invocation: com.vladmihalcea.cache.FibonacciService.compute([0]) Calculate fibonacci for number 1 Memoizing 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 3 Using 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 5 Using 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 7 Using 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 9 Using 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 Влада Михалча в блоге Влада Михалча . |