Рубрики
Без рубрики

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

Автор оригинала: Vlad Mihalcea.

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

В этом посте будет продемонстрировано, как вы можете выполнять повторяемые чтения на уровне запроса для любого источника данных, используя только Spring AOP .

Spring предлагает очень полезную абстракцию кэширования , позволяющую отделить логику приложения от деталей реализации кэширования.

Весеннее кэширование использует область применения на уровне приложения, поэтому для запоминания только по запросу нам нужно использовать подход DIY .

Жизненный цикл записи кэша на уровне запроса всегда привязан к текущей области запроса. Такой кэш очень похож на контекст сохранения в режиме гибернации, который предлагает повторяемые чтения на уровне сеанса .

Повторяемые чтения являются обязательными для предотвращения потери обновлений , даже для решений NoSQL.

Сначала мы определим аннотацию маркера запоминания:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Memoize {
}

В этой аннотации будут явно отмечены все методы, которые необходимо запомнить.

Чтобы различать различные вызовы методов, мы собираемся инкапсулировать информацию о вызове метода в следующий тип объекта:

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 Запрос / Сессия бобовые области.

Поскольку нам требуется область запоминания на уровне запроса, мы можем упростить наш дизайн с помощью области запроса Spring, которая скрывает фактическую логику разрешения HttpSession:

@Component
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "request")
public class RequestScopeCache {

    public static final Object NONE = new Object();

    private final Map cache = new HashMap();

    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
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;
    }
}

Давайте проверим все это на практике. Для простоты мы собираемся эмулировать требования к запоминанию области на уровне запроса с помощью калькулятора чисел Фибоначчи:

@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);
    }
}

Контекст приложения Spring, используемый для этого теста, выглядит следующим образом:




    

    

    

    
        
            
                
                    
                
            
        
    


Если мы рассчитаем 10-е число Фибоначчи, мы получим следующий результат:

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 .