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