Multilevel cache architecture development 7 (add annotation function to multilevel cache Development)

Keywords: github Java Spring calculator

Purpose:

In order to solve the problem of code intrusion, annotation + Spring Aop is used to realize the pluggability of caching the project

Implementation steps:

1. Import jar package

Add under pom file of parent module

    <dependencyManagement>
        <dependencies>
 
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>${aspectj.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>${spring.version}</version>
            </dependency>
 
        </dependencies>
    </dependencyManagement>

Add under pom file of submodule

    <dependencies>

        <dependency>
            <groupId>org.github.roger</groupId>
            <artifactId>multi-layering-cache-core</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>

    </dependencies>

2. Development cache comments

2.1) define level 1 cache configuration item annotation

package com.github.roger.annotation;

import org.github.roger.enumeration.ExpireMode;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
    L1 cache configuration item
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface FirstCache {

    /**
     * Cache initial Size
     */
    int initialCapacity() default  10;

    /**
     * Cache maximum Size
     */
    int maximumSize() default  5000;

    /**
     * Cache effective time
     */
    int expireTime() default  9;

    /**
     * Cache time unit
     */
    TimeUnit timeUnit() default TimeUnit.MINUTES;

    /**
     * Cache expiration mode {@ link expiermode}
     */
    ExpireMode expireMode() default ExpireMode.WRITE;
}

2.2) define L2 cache configuration item annotation

package com.github.roger.annotation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
    L2 cache configuration item
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface SecondaryCache {

    /**
     * Cache effective time
     */
    long expiration() default 0;

    /**
     * The time when the cache proactively forces the cache to refresh before it expires
     * The suggestion is: preloadTime default expireTime * 0.2
     *
     * @return long
     */
    long preloadTime() default 0;

    /**
     * Time unit {@ link TimeUnit}
     */
    TimeUnit timeUnit() default TimeUnit.HOURS;

    /**
     * Force refresh (go to database), default is false
     */
    boolean forceRefresh() default false;


    /**
     * Allow NULL value
     */
    boolean allowNullValue() default false;

    /**
     * Time multiplier between non null and null values, default is 1. allowNullValuedefaulttrue is valid
     *
     * If the effective time of the configuration cache is 200 seconds, the multiplier is set to 10,
     * When the cache value is null, the cache effective time will be 20 seconds, and the non empty time will be 200 seconds
     */
    int magnification() default 1;
}

2.3) defines comments that can be cached for the return results of a method or all methods of a class

package com.github.roger.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * Represents that the results of the called method (or all methods in the class) can be cached.
 * When the method is called, check whether the cache hits first. If not, call the cached method and put its return value into the cache.
 * Here, both value and key support SpEL expression
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Cacheable {

    /**
     * The alias is {@ link ා cachenames}
     *
     * @return String[]
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Cache name, SpEL expression supported
     *
     * @return String[]
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Cache key, support for SpEL expression
     *
     * @return String
     */
    String key() default "";


    /**
     * Whether to ignore the exceptions encountered in the operation cache, such as deserialization exception. The default is true.
     * <p>true: If there is an exception, the warn level log will be output and the cached method will be executed directly (the cache will be invalidated)</p>
     * <p>false:If there is an exception, the error level log will be output and an exception will be thrown</p>
     *
     * @return boolean
     */
    boolean ignoreException() default true;

    /**
     * L1 cache configuration
     *
     * @return FirstCache
     */
    FirstCache firstCache() default @FirstCache();

    /**
     * L2 cache configuration
     *
     * @return SecondaryCache
     */
    SecondaryCache secondaryCache() default @SecondaryCache();
}

2.4) define comments to add cache

package com.github.roger.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ElementType.METHOD})
public @interface CachePut {
    /**
     * The alias is {@ link ා cachenames}
     *
     * @return String[]
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Cache name, SpEL expression supported
     *
     * @return String[]
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Cache key, support for SpEL expression
     *
     * @return String
     */
    String key() default "";


    /**
     * Whether to ignore the exceptions encountered in the operation cache, such as deserialization exception. The default is true.
     * <p>true: If there is an exception, the warn level log will be output and the cached method will be executed directly (the cache will be invalidated)</p>
     * <p>false:If there is an exception, the error level log will be output and an exception will be thrown</p>
     *
     * @return boolean
     */
    boolean ignoreException() default true;

    /**
     * L1 cache configuration
     *
     * @return FirstCache
     */
    FirstCache firstCache() default @FirstCache();

    /**
     * L2 cache configuration
     *
     * @return SecondaryCache
     */
    SecondaryCache secondaryCache() default @SecondaryCache();
}

2.5) define delete cached comments

package com.github.roger.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target({ElementType.METHOD})
public @interface CacheEvict {
    /**
     * The alias is {@ link ා cachenames}
     *
     * @return String[]
     */
    @AliasFor("cacheNames")
    String[] value() default {};

    /**
     * Cache name, SpEL expression supported
     *
     * @return String[]
     */
    @AliasFor("value")
    String[] cacheNames() default {};

    /**
     * Cache key, support for SpEL expression
     *
     * @return String
     */
    String key() default "";


    /**
     * Whether to ignore the exceptions encountered in the operation cache, such as deserialization exception. The default is true.
     * <p>true: If there is an exception, the warn level log will be output and the cached method will be executed directly (the cache will be invalidated)</p>
     * <p>false:If there is an exception, the error level log will be output and an exception will be thrown</p>
     *
     * @return boolean
     */
    boolean ignoreException() default true;

    /**
     * Delete all data in cache
     * <p>By default, only the cache data of the associated key is deleted
     * <p>Note: when the parameter is set to {@ code true}, the {@ link ා key} parameter will be invalid
     *
     * @return boolean
     */
    boolean allEntries() default false;
}

3. Create the SpEL expression calculator

3.1) defines a class that describes the root object used during expression evaluation

package com.github.roger.expression;

import lombok.Getter;
import org.springframework.util.Assert;

import java.lang.reflect.Method;

/**
 * Describes the root object used during expression evaluation.
 */
@Getter
public class CacheExpressionRootObject {
    private final Method method;

    private final Object[] args;

    private final Object target;

    private final Class<?> targetClass;

    public CacheExpressionRootObject(Method method, Object[] args, Object target, Class<?> targetClass) {
        Assert.notNull(method, "Method is required");
        Assert.notNull(targetClass, "targetClass is required");
        this.method = method;
        this.target = target;
        this.targetClass = targetClass;
        this.args = args;
    }

    public String getMethodName() {
        return this.method.getName();
    }
}

3.2) get target method

    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if(targetMethod == null){
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            if (targetMethod == null) {
                targetMethod = method;
            }
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }

3.3) defines a context for cache object evaluation, which is added as a SpEL variable using the parameters of the method

package com.github.roger.expression;

import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.ParameterNameDiscoverer;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

public class CacheEvaluationContext extends MethodBasedEvaluationContext {

    private final Set<String> unavailableVariables = new HashSet<String>(1);

    public CacheEvaluationContext(CacheExpressionRootObject rootObject, Method targetMethod, Object[] args, ParameterNameDiscoverer parameterNameDiscoverer) {
        super(rootObject, targetMethod, args, parameterNameDiscoverer);
    }

    public void addUnavailableVariable(String name) {
        this.unavailableVariables.add(name);
    }

    @Override
    public Object lookupVariable(String name) {
        if(this.unavailableVariables.contains(name)){
            throw new VariableNotAvailableException(name);
        }
        return super.lookupVariable(name);
    }
}

3.4) use the above three small steps to form a usage class

package com.github.roger.expression;

import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator {

    /**
     * Indicates no result variable
     */
    public static final Object NO_RESULT = new Object();

    /**
     * Indicates that the result variable is not available at all
     */
    public static final Object RESULT_UNAVAILABLE = new Object();

    /**
     *  The name of the variable that holds the result object.
     */
    public static final String RESULT_VARIABLE = "result";

    private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);

    private final Map<ExpressionKey, Expression> cacheNameCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);

    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);

    private final Map<ExpressionKey, Expression> unlessCache = new ConcurrentHashMap<ExpressionKey, Expression>(64);

    private final Map<AnnotatedElementKey, Method> targetMethodCache =
            new ConcurrentHashMap<AnnotatedElementKey, Method>(64);

    public EvaluationContext createEvaluationContext(Method method, Object[] args, Object target, Class<?> targetClass) {

        return createEvaluationContext(method, args, target, targetClass, NO_RESULT);
    }

    public EvaluationContext createEvaluationContext(Method method, Object[] args,
                                                     Object target, Class<?> targetClass, Object result) {

        CacheExpressionRootObject rootObject = new CacheExpressionRootObject(
                method, args, target, targetClass);
        Method targetMethod = getTargetMethod(targetClass, method);
        CacheEvaluationContext evaluationContext = new CacheEvaluationContext(
                rootObject, targetMethod, args, getParameterNameDiscoverer());
        if (result == RESULT_UNAVAILABLE) {
            evaluationContext.addUnavailableVariable(RESULT_VARIABLE);
        } else if (result != NO_RESULT) {
            evaluationContext.setVariable(RESULT_VARIABLE, result);
        }
        return evaluationContext;
    }


    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if(targetMethod == null){
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            if (targetMethod == null) {
                targetMethod = method;
            }
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }


    public Object key(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {

        return getExpression(this.keyCache, methodKey, expression).getValue(evalContext);
    }

    public Object cacheName(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {

        return getExpression(this.cacheNameCache, methodKey, expression).getValue(evalContext);
    }

    public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
        return getExpression(this.conditionCache, methodKey, conditionExpression).getValue(evalContext, boolean.class);
    }

    public boolean unless(String unlessExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
        return getExpression(this.unlessCache, methodKey, unlessExpression).getValue(evalContext, boolean.class);
    }

    /**
     * Clear all caches.
     */
    void clear() {
        this.keyCache.clear();
        this.conditionCache.clear();
        this.unlessCache.clear();
        this.targetMethodCache.clear();
    }
}

4. Develop aop facets for each annotation

3.1) define the tangent point of annotation type for each annotation

3.2) define wrapping method for tangent point of each cache annotation

3.2.1) define the SpEL expression calculator, which is used to obtain the value of the key attribute configured by the annotation and construct the value of the cache key

3.2.2) when defining the default key attribute configuration of an annotation, the value of the default composition cache key is generated

3.2.3) inject the multi-level cache manager in the multi-layer cache core module to operate the cache

package com.github.roger.aspect;

import com.github.roger.annotation.CacheEvict;
import com.github.roger.annotation.CachePut;
import com.github.roger.annotation.Cacheable;
import com.github.roger.expression.CacheOperationExpressionEvaluator;
import com.github.roger.key.KeyGenerator;
import com.github.roger.key.impl.DefaultKeyGenerator;
import com.github.roger.support.CacheOperationInvoker;
import com.github.roger.utils.CacheAspectUtil;
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.github.roger.cache.ICache;
import org.github.roger.exception.SerializationException;
import org.github.roger.manager.ICacheManager;
import org.github.roger.settings.FirstCacheSetting;
import org.github.roger.settings.MultiLayeringCacheSetting;
import org.github.roger.settings.SecondaryCacheSetting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Method;
import java.util.Collection;

@Aspect
@Slf4j
public class MultiLayeringCacheAspect {

    private static final String CACHE_KEY_ERROR_MESSAGE = "cache Key %s Can not be NULL";
    private static final String CACHE_NAME_ERROR_MESSAGE = "Cache name cannot be NULL";

    @Autowired(required = false)//Use custom if custom, otherwise use default
    private KeyGenerator keyGenerator = new DefaultKeyGenerator();

    @Autowired
    private ICacheManager iCacheManager;

    @Pointcut("@annotation(com.github.roger.annotation.Cacheable)")
    public void cacheablePointCut(){
    }

    @Pointcut("@annotation(com.github.roger.annotation.CachePut")
    public void cachePutPointCut(){
    }

    @Pointcut("@annotation(com.github.roger.annotation.CacheEvict")
    public void cacheEvictPointCut(){
    }

    @Around("cacheablePointCut()")
    public Object cacheableAroundAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{
        //Operation class interface to get data through non cached way
        CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint);

        //Get executing target method
        Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint);

        //Get Cacheable annotation on method
        Cacheable cacheable = AnnotationUtils.findAnnotation(method,Cacheable.class);
        try {
            //How to execute query caching
            return executeCachealbe(aopInvoker, method, cacheable, pJoinPoint);
        }catch (SerializationException sex){
            // If it is a serialization exception, you need to delete the original cache first
            String[] cacheNames = cacheable.cacheNames();
            // Delete cache
            delete(cacheNames, cacheable.key(), method, pJoinPoint);

            // Ignore exceptions encountered during operation caching
            if (cacheable.ignoreException()) {
                log.warn(sex.getMessage(), sex);
                return aopInvoker.invoke();
            }
            throw sex;
        }catch (Exception ex){
            // Ignore exceptions encountered during operation caching
            if (cacheable.ignoreException()) {
                log.warn(ex.getMessage(), ex);
                return aopInvoker.invoke();
            }
            throw ex;
        }
    }

    private Object executeCachealbe(CacheOperationInvoker aopInvoker, Method method, Cacheable cacheable, ProceedingJoinPoint pJoinPoint) {
        // Parse the SpEL expression to get the cacheName and key
        String[] cacheNames = cacheable.cacheNames();
        Assert.notEmpty(cacheable.cacheNames(), CACHE_NAME_ERROR_MESSAGE);
        String cacheName = cacheNames[0];

        Object key = CacheAspectUtil.generateKey(keyGenerator,cacheable.key(), method, pJoinPoint);
        Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, cacheable.key()));

        // Construct multilevel cache configuration information
        MultiLayeringCacheSetting layeringCacheSetting = CacheAspectUtil.generateMultiLayeringCacheSetting(cacheable.firstCache(),cacheable.secondaryCache());

        // Get Cache through cacheName and Cache configuration
        ICache iCache = iCacheManager.getCache(cacheName, layeringCacheSetting);

        // Get value through Cache
        return iCache.get(key, () -> aopInvoker.invoke());

    }

    @Around("cacheEvictPointCut()")
    public Object cacheEvicArountAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{
        //Operation class interface to get data through non cached way
        CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint);

        //Get executing target method
        Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint);

        //Get Cacheable annotation on method
        CacheEvict cacheEvict = AnnotationUtils.findAnnotation(method,CacheEvict.class);
        try{
            return executeCacheEvict(aopInvoker,method,cacheEvict,pJoinPoint);
        }catch (Exception ex){
            // Ignore exceptions encountered during operation caching
            if (cacheEvict.ignoreException()) {
                log.warn(ex.getMessage(), ex);
                return aopInvoker.invoke();
            }
            throw ex;
        }
    }

    private Object executeCacheEvict(CacheOperationInvoker aopInvoker, Method method, CacheEvict cacheEvict, ProceedingJoinPoint pJoinPoint) throws Throwable {
        // Parse the SpEL expression to get the cacheName and key
        String[] cacheNames = cacheEvict.cacheNames();
        Assert.notEmpty(cacheEvict.cacheNames(), CACHE_NAME_ERROR_MESSAGE);
        // Determine whether to delete all cached data
        if(cacheEvict.allEntries()){
            // Delete all cached data (empty)
            for (String cacheName : cacheNames) {
                Collection<ICache> iCaches = iCacheManager.getCache(cacheName);
                if (CollectionUtils.isEmpty(iCaches)) {
                    // If Cache is not found, create a new default
                    ICache iCache = iCacheManager.getCache(cacheName,
                            new MultiLayeringCacheSetting(new FirstCacheSetting(), new SecondaryCacheSetting()));
                    iCache.clear();
                } else {
                    for (ICache iCache : iCaches) {
                        iCache.clear();
                    }
                }
            }
        }else{
            delete(cacheNames,cacheEvict.key(),method,pJoinPoint);
        }
        return aopInvoker.invoke();
    }


    /**
     * Delete the specified key on the execution cache name
     * */
    private void delete(String[] cacheNames, String keySpEL, Method method, ProceedingJoinPoint pJoinPoint) {
        Object key = CacheAspectUtil.generateKey(keyGenerator,keySpEL, method, pJoinPoint);
        Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, keySpEL));
        for (String cacheName : cacheNames) {
            Collection<ICache> iCaches = iCacheManager.getCache(cacheName);
            if (CollectionUtils.isEmpty(iCaches)) {
                // If Cache is not found, create a new default
                ICache iCache = iCacheManager.getCache(cacheName,
                        new MultiLayeringCacheSetting(new FirstCacheSetting(), new SecondaryCacheSetting()));
                iCache.evict(key);
            } else {
                for (ICache iCache : iCaches) {
                    iCache.evict(key);
                }
            }
        }
    }

    @Around("cachePutPointCut()")
    public Object cachePutAroundAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{

        //Operation class interface to get data through non cached way
        CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint);

        //Get executing target method
        Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint);

        //Get CachePut annotation on method
        CachePut cachePut = AnnotationUtils.findAnnotation(method,CachePut.class);

        try {
            // Execute query cache method
            return executeCachePut(aopInvoker, method, cachePut, pJoinPoint);
        } catch (Exception e) {
            // Ignore exceptions encountered during operation caching
            if (cachePut.ignoreException()) {
                log.warn(e.getMessage(), e);
                return aopInvoker.invoke();
            }
            throw e;
        }

    }

    private Object executeCachePut(CacheOperationInvoker aopInvoker, Method method, CachePut cachePut, ProceedingJoinPoint pJoinPoint) throws Throwable{

        // Parse the SpEL expression to get the cacheName and key
        String[] cacheNames = cachePut.cacheNames();
        Assert.notEmpty(cachePut.cacheNames(), CACHE_NAME_ERROR_MESSAGE);

        Object key = CacheAspectUtil.generateKey(keyGenerator,cachePut.key(), method, pJoinPoint);
        Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, cachePut.key()));

        // Construct multilevel cache configuration information
        MultiLayeringCacheSetting layeringCacheSetting = CacheAspectUtil.generateMultiLayeringCacheSetting(cachePut.firstCache(),cachePut.secondaryCache());

        // Specifies the call method to get the cache value
        Object result = aopInvoker.invoke();
        for (String cacheName : cacheNames) {
            // Get Cache through cacheName and Cache configuration
            ICache iCache = iCacheManager.getCache(cacheName, layeringCacheSetting);
            iCache.put(key, result);
        }
        return result;
    }
}

 

Posted by helpwanted on Fri, 08 Nov 2019 10:31:30 -0800