Статьи

Весеннее напоминание на уровне запроса

Вступление

Мемоизация — это метод кэширования на уровне метода для ускорения последовательных вызовов.

В этом посте будет продемонстрировано, как можно добиться повторяемого чтения на уровне запросов для любого источника данных, используя только 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 Влада Михалча в блоге Влада Михалча .