Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get dynamic parameter referenced in Annotation by using Spring SpEL Expression

What I am trying to do is to have an Annotation which looks a lot like the @Cacheable Annotation Spring is providing.

Used on top of a method it looks like the following:

@CleverCache(key = "'orders_'.concat(#id)")
public Order getOrder(int id) {

When I use the same using Cacheable it is somehow able to interpret this SpEL-Expression and generate a key which has the value orders_1234 (for id=1234)

My matching advice looks like the following:

@Around("CleverCachePointcut(cleverCache)")
public Object clevercache(ProceedingJoinPoint joinPoint, CleverCache cleverCache) throws Throwable {
    String expression = cleverCache.key();
    //FIXME: Please add working code here :D - extracting the key by interpreting the passed SpEL Expression in expression

I definitly get the expression there, but I didn't yet figure out how to make it work that it is correctly interpreting the SpEL-Expression.

Another support syntax should be key = "T(com.example.Utils).createCacheKey(#paramOfMethodByName)" where the a static helper for creating a key is invoked.

Any idea how this could work? The code where I have the snippets from is available at: https://github.com/eiselems/spring-redis-two-layer-cache/blob/master/src/main/java/com/marcuseisele/example/twolayercache/clevercache/ExampleAspect.java#L35

Any help is really appreciated!

like image 424
eiselems Avatar asked Dec 17 '18 20:12

eiselems


1 Answers

It is actually quite simple to evaluate SpEL, if you have the necessary context information. Please refer to this article in order to find out how to programmatically parse SpEL.

As for that context information, you did not explain much about the types of methods you annotated by @CleverCache. The thing is, the pointcut intercepts all annotated methods and I do not know if each one's first parameter is an int ID. Depending on the answer to this question it is easier (just one simple case) or more difficult (you need if-else in order to just find the methods with an integer ID) to get the ID argument value from the intercepted method. Or maybe you have all sorts of expressions referencing multiple types and names of method parameters, instance variables or whatever. The solution's complexity is linked to the requirements' complexity. If you provide more info, maybe I can also provide more help.


Update: Having looked at your GitHub repo, I refactored your aspect for the simple case:

package com.marcuseisele.example.twolayercache.clevercache;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class ExampleAspect {
    private static final ExpressionParser expressionParser = new SpelExpressionParser();

    private Map<String, RedisTemplate> templates;

    public ExampleAspect(Map<String, RedisTemplate> redisTemplateMap) {
        this.templates = redisTemplateMap;
    }

    @Pointcut("@annotation(cleverCache)")
    public void CleverCachePointcut(CleverCache cleverCache) {
    }

    @Around("CleverCachePointcut(cleverCache) && args(id)")
    public Object clevercache(ProceedingJoinPoint joinPoint, CleverCache cleverCache, int id) throws Throwable {
        long ttl = cleverCache.ttl();
        long grace = cleverCache.graceTtl();

        String key = cleverCache.key();
        Expression expression = expressionParser.parseExpression(key);
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("id", id);
        String cacheKey = (String) expression.getValue(context);
        System.out.println("### Cache key: " + cacheKey);

        long start = System.currentTimeMillis();
        RedisTemplate redisTemplate = templates.get(cleverCache.redisTemplate());
        Object result;
        if (redisTemplate.hasKey(cacheKey)) {
            result = redisTemplate.opsForValue().get(cacheKey);
            log.info("Reading from cache ..." + result.toString());

            if (redisTemplate.getExpire(cacheKey, TimeUnit.MINUTES) < grace) {
                log.info("Entry is in Grace period - trying to refresh it");
                try {
                    result = joinPoint.proceed();
                    redisTemplate.opsForValue().set(cacheKey, result, grace+ttl, TimeUnit.MINUTES);
                    log.info("Fetch was successful - new value will be returned");
                } catch (Exception e) {
                    log.warn("An error occured while trying to refresh the value - extending the old one", e);
                    //TODO: think about only adding 5 minutes on top of grace, or 50% of ttl on top of grace
                    //if protected by a circuit breaker we could go REALLY low here
                    redisTemplate.opsForValue().getOperations().expire(cacheKey, grace+ttl, TimeUnit.MINUTES);
                }

            }

        } else {
            result = joinPoint.proceed();
            log.info("Giving from method ..." + result.toString());
            redisTemplate.opsForValue().set(cacheKey, result, ttl + grace, TimeUnit.MINUTES);
        }

        long executionTime = System.currentTimeMillis() - start;
        log.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
        log.info("Result: {}", result);
        return result;
    }
}

The diff looks like this:

Diff screenshot

like image 61
kriegaex Avatar answered Oct 29 '22 00:10

kriegaex