自定义缓存注解,动态缓存数据
此注解解决的业务功能:
类似于Cacheable的功能,在方法上添加注解能够动态的缓存起来,只不过我这里自定义了注解,并用redis缓存数据,数据在存储前可以派生其他的类来解决动态的修改自己的缓存业务逻辑。
此注解产生的原因:
线上百万级访问量访问接口,由于各种原因扛不住压力,所以需要解决问题,所以决定采用缓存处理,但是缓存处理因为业务逻辑众多,又不想嵌入具体的代码进行逻辑判断,所以直接利用注解AOP动态判断最后的结果然后对最后的结果进行缓存。但是由于有些数据是不存在的但又要一些空结构来保持前端的需求,才会产生派生类来维持数据机构。
具体代码:
注解 @RedisCacheable
/** * redis 动态缓存读取 * 如果数据不存在则存储返回值到redis * 如果redis存在数据,则直接返回 * attribute key支持# 字符el表达式 * * * \@RedisCacheable(key = "#userId",timeout = 60) * public User queryUserInfo(Integer userId){ * ... * } *
* * @author licl */@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface RedisCacheable { /** * redis key * # 字符支持 EL表达式 */ String key(); /** * 超时时间秒数 second */ long timeout() default -1; /** * 接口描述 */ String describe() default "Interface description"; /** * 自定义处理 * 用于处理数据存储前的数据格式 */ Class process() default DefaultRedisCacheProccessor.class;}
定义抽象处理类:
import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;/** * redisCache注解 自定义处理抽象类 * * @author licl */public abstract class AbstractRedisCacheProccessor { /** * 前置处理 * * @param obj process * @param redisKey redisKey * @return 处理后的process */ abstract Object beforeProcess(Object obj, String redisKey); /** * 缓存数据 * * @param redisTemplate redisTemplate * @param proceedproceed * @param redisKey redisKey * @param timeouttimeout */ public void process(StringRedisTemplate redisTemplate, Object proceed, String redisKey, Long timeout) { this.check(proceed); redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed, SerializerFeature.WriteMapNullValue), timeout, TimeUnit.SECONDS); } /** * 检查数据是否为空 * * @param obj process */ private void check(Object obj) { if (null == obj) throw new RuntimeException("data cannot be empty"); }}
抽象类默认实现:
import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.util.CollectionUtils;import java.util.List;import java.util.Map;/** * 默认实现 */public class DefaultRedisCacheProccessor extends AbstractRedisCacheProccessor { /** * 前置处理 */ @Override public Object beforeProcess(Object obj, String redisKey) { return obj; } /** * 处理 */ @Override public void process(StringRedisTemplate redisTemplate, Object proceed, String redisKey, Long timeout) { super.process(redisTemplate, proceed, redisKey, timeout); }}
我已经把默认处理逻辑删除了,你可以替换成你自己的默认逻辑,然后在新增其他的派生类,集成这个默认类,在具体的派生类里面写处理逻辑。
核心AOP处理类:
import com.alibaba.excel.util.StringUtils;import com.alibaba.fastjson.JSON;import lombok.extern.log4j.Log4j2;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.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.LocalVariableTableParameterNameDiscoverer;import org.springframework.data.redis.core.StringRedisTemplate;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.lang.reflect.Method;import java.lang.reflect.ParameterizedType;import java.lang.reflect.Type;import java.util.concurrent.ConcurrentHashMap;/** * RedisCacheable aop * * @author licl */@Log4j2@Aspect@Componentpublic class RedisCacheableAspect { /** * 实例缓存 */ private final static ConcurrentHashMap PROCESS_INSTANCE = new ConcurrentHashMap(); /** * 实例方法缓存 */ private final static ConcurrentHashMap PROCESS_METHODS = new ConcurrentHashMap(); @Autowired private StringRedisTemplate redisTemplate; @Pointcut("@annotation(RedisCacheable)") private void aroundMethodAspect() { } @Around(value = "aroundMethodAspect()") public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); RedisCacheable handler = method.getAnnotation(RedisCacheable.class);// AssertUtils.isTrue(StringUtils.isNotBlank(handler.key()), "annotation RedisCacheable key cannot be empty");// AssertUtils.isTrue(handler.timeout() > 0, "annotation RedisCacheable timeout value must be greater than 0"); StandardEvaluationContext context = parseElContext(method, joinPoint.getArgs()); String redisKey = this.getRedisKey(context, handler); String result = redisTemplate.opsForValue().get(redisKey); if (StringUtils.isNotBlank(result)) { log.info("【{}】获取缓存数据 data = {}", handler.describe(), result); Type genericReturnType = method.getGenericReturnType(); if (genericReturnType instanceof ParameterizedType) { Class rawType = (Class) ((ParameterizedType) genericReturnType).getRawType(); return JSON.parseObject(result, rawType); } return JSON.parseObject(result, Class.forName(genericReturnType.getTypeName())); } else { Object proceed = joinPoint.proceed(); if (null != proceed) { return process(joinPoint, handler, redisKey); } return proceed; } } /** * 执行处理程序 */ private Object process(ProceedingJoinPoint joinPoint, RedisCacheable handler, String redisKey) throws Throwable { Object proceed = joinPoint.proceed(); Class process = handler.process(); Object instance = getInstance(process); Method beforeProcess = getMethod(process, "beforeProcess", Object.class, String.class); Method processMain = getMethod(process, "process", StringRedisTemplate.class, Object.class, String.class, Long.class); Object data = beforeProcess.invoke(instance, proceed, redisKey); processMain.invoke(instance, redisTemplate, data, redisKey, handler.timeout()); return data; } public Method getMethod(Class process, String methodName, Class... parameterTypes) throws Exception { PROCESS_METHODS.clear(); String name = process.getName(); // String key = name + "." + methodName + "(" + formatParameterName(parameterTypes) + ")"; Method oldMethod = PROCESS_METHODS.get(key); if (null == oldMethod) { Method method = process.getMethod(methodName, parameterTypes); PROCESS_METHODS.put(key, method); } return PROCESS_METHODS.get(key); } public Object getInstance(Class process) throws Exception { String name = process.getName(); Object instance = PROCESS_INSTANCE.get(name); if (null == instance) { PROCESS_INSTANCE.put(name, process.newInstance()); } return PROCESS_INSTANCE.get(name); } public String formatParameterName(Class... parameterTypes) { StringBuilder parameter = new StringBuilder(StringUtils.EMPTY); if (null == parameterTypes || parameterTypes.length == 0) { return parameter.toString(); } for (Class parameterType : parameterTypes) { String parameterToString = parameterType.toString(); parameter.append(parameterToString.replace("class ", StringUtils.EMPTY)).append(","); } parameter.deleteCharAt(parameter.length() - 1); return parameter.toString(); } /** * 解析redisKey */ private String getRedisKey(StandardEvaluationContext context, RedisCacheable handler) { //使用SPEL进行key的解析 ExpressionParser parser = new SpelExpressionParser(); String redisKeySpel = handler.key(); if (redisKeySpel.contains("#")) { return parser.parseExpression(redisKeySpel).getValue(context, String.class); } else { return redisKeySpel; } } /** * 内容解析 */ private StandardEvaluationContext parseElContext(Method method, Object[] args) { //获取被拦截方法参数名列表(使用Spring支持类库) LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = localVariableTable.getParameterNames(method); if (null != paraNameArr && paraNameArr.length > 0) { //SPEL上下文 StandardEvaluationContext context = new StandardEvaluationContext(); //把方法参数放入SPEL上下文中 for (int i = 0; i < paraNameArr.length; i++) { context.setVariable(paraNameArr[i], args[i]); } return context; } return null; }}
此代码直接可用。
原理就是扫描注解,然后解析key的 el表达式,然后执行前置缓存处理逻辑,处理完毕后在进行缓存。完全不侵入代码判断。