redis缓存切面实现(支持缓存key的spel表达式)
1.定义注解
package com.g2.order.server.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * redis缓存注解 * 仅支持方法 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisCachetAttribute { /** * @return 缓存的key值 * 对应的Method的返回值必须 实现 Serializable 接口 * */ String key(); /** * 到期秒数 * * @return 到期秒数 */ int expireSeconds() default 20; }
2.定义切面
package com.g2.order.server.aspect; import com.g2.order.server.annotation.RedisCachetAttribute; import com.g2.order.server.utils.ObjectUtils; import org.aspectj.lang.JoinPoint; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.Order; import org.springframework.expression.EvaluationContext; 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 redis.clients.jedis.JedisCluster; //开启AspectJ 自动代理模式,如果不填proxyTargetClass=true,默认为false, @EnableAspectJAutoProxy(proxyTargetClass = true) @Component @Order(-1) @Aspect public class RedisCacheAspect { /** * 日志 */ private static Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class); /** * SPEL表达式解析器 */ private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); /** * 获取方法参数名称发现器 */ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); /** * Redis集群 */ @Autowired private JedisCluster jedisCluster; /** * 切面切入点 */ @Pointcut("@annotation(com.g2.order.server.annotation.RedisCachetAttribute)") public void mergeDuplicationRequest() { } /** * 环绕切面 */ @Around("mergeDuplicationRequest()") public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { //获取controller对应的方法. MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature(); //获取方法 Method method = methodSignature.getMethod(); //获取注解 RedisCachetAttribute annotation = method.getAnnotation(RedisCachetAttribute.class); //获取缓存key的表达式,并根据上下文等数据计算表达式 String cacheKey = parseKey(annotation.key(), proceedingJoinPoint); int seconds = annotation.expireSeconds(); //先尝试从redis里获取数据(字节) byte[] redisKey = cacheKey.getBytes(); byte[] redisValue = jedisCluster.get(redisKey); if (redisValue != null && redisValue.length > 0) { //redis有数据,直接返回 return ObjectUtils.toObject(redisValue); } //redis没有数据,则调用原方法,获取结果值 Object result = proceedingJoinPoint.proceed(); //将返回值序列化为Byte[],保存到redis redisValue = ObjectUtils.toByteArray(result); jedisCluster.setex(redisKey, seconds, redisValue); return result; } /** * 计算spel表达式 * * @param expression 表达式 * @param context 上下文 * @return String的缓存key */ private static String parseKey(String expression, JoinPoint context) { //获取切入点的方法信息 MethodSignature methodSignature = (MethodSignature) context.getSignature(); Method method = methodSignature.getMethod(); // 获取传入参数值 Object[] args = context.getArgs(); if (args == null || args.length == 0) { // 无参传入,直接计算表达式(无需参数上下文) return EXPRESSION_PARSER.parseExpression(expression).getValue(String.class); } // 获取参数名 String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); if (parameterNames.length > args.length) { //由于java不允许有匿名参数,所以如果参数名多于参数值,则必为非法 logger.error("参数值的长度少于参数名长度, 方法:{}, 参数名长度: {},参数值长度:{}", method, parameterNames.length, args.length); throw new IllegalArgumentException("参数传入不足"); } // 将参数名与参数值放入参数上下文 EvaluationContext evaluationContext = new StandardEvaluationContext(); for (int i = 0; i < parameterNames.length; i++) { evaluationContext.setVariable(parameterNames[i], args[i]); } // 计算表达式(根据参数上下文) return EXPRESSION_PARSER.parseExpression(expression).getValue(evaluationContext, String.class); } }
3.引用代码
package com.g2.order.server.business; import com.google.common.collect.ImmutableMap; import com.g2.order.server.annotation.RedisCachetAttribute; import com.g2.order.server.business.vo.JsonResult; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; import java.util.Map; /** * SettingBusiness. */ @Component public class SettingBusiness { @RedisCachetAttribute(key = "T(com.g2.order.server.vo.CommonConst)" + ".SETTING_JSON_KEY" + " + #code" , expireSeconds = 30 ) public JsonResult getSetting(String code) { return new JsonResult("234"); } @RedisCachetAttribute(key = "T(com.g2.order.server.vo.CommonConst)" + ".SETTING_LIST_KEY" + " + #code" , expireSeconds = 30 ) public List<JsonResult> getSettingList(String code) { return Arrays.<JsonResult>asList(new JsonResult("234"), new JsonResult("345")); } @RedisCachetAttribute(key = "T(com.g2.order.server.vo.CommonConst)" + ".SETTING_MAP_KEY" + " + #code" , expireSeconds = 30 ) public Map<String, JsonResult> getSettingMap(String code) { return ImmutableMap.of(code, new JsonResult("234"),"abc",new JsonResult("abc234")); } }
package com.g2.order.server.business.vo; import java.io.Serializable; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Model */ @AllArgsConstructor @NoArgsConstructor @Data public class JsonResult implements Serializable { private Object data; }
4.测试如下.(验证获取普通POJO,List,Map的返回结构)
package com.g2.order.server.controller; import com.g2.order.server.business.SettingBusiness; import com.g2.order.server.business.vo.JsonResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; import io.swagger.annotations.Api; @Api(value = "H5Controller", description = "H5接口") @RestController @RequestMapping("/h5") public class H5Controller { private static Logger logger = LoggerFactory.getLogger(H5Controller.class); @Autowired private SettingBusiness settingBusiness; @ResponseBody //@MergeDuplicationRequestAttribute(redisLockKeyTemplate = "A:%s", redisLockKeyObjectFileds = {"code"}) @RequestMapping(value = "/{code}.jsonp", method = RequestMethod.GET) public Object testJsonp1(@PathVariable("code") String code) { // return JsonMapper.INSTANCE.toJsonP("callabc111", new JsonResult("234")); JsonResult result = settingBusiness.getSetting(code); logger.info("获取结果,参数:{},值:{}", code, result); return result; } @ResponseBody //@MergeDuplicationRequestAttribute(redisLockKeyTemplate = "A:%s", redisLockKeyObjectFileds = {"code"}) @RequestMapping(value = "/{code}.LIST", method = RequestMethod.GET) public Object testJsonp2(@PathVariable("code") String code) { // return JsonMapper.INSTANCE.toJsonP("callabc111", new JsonResult("234")); List<JsonResult> result = settingBusiness.getSettingList(code); logger.info("获取结果,参数:{},值:{}", code, result); return result; } @ResponseBody //@MergeDuplicationRequestAttribute(redisLockKeyTemplate = "A:%s", redisLockKeyObjectFileds = {"code"}) @RequestMapping(value = "/{code}.MAP", method = RequestMethod.GET) public Object testJsonp3(@PathVariable("code") String code) { // return JsonMapper.INSTANCE.toJsonP("callabc111", new JsonResult("234")); Map<String,JsonResult> result = settingBusiness.getSettingMap(code); logger.info("获取结果,参数:{},值:{}", code, result); return result; } }
5.辅助代码
package com.g2.order.server.utils; import com.google.common.collect.Lists; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** * Object帮助类 */ public class ObjectUtils { /** * 对象转Byte数组 */ public static byte[] toByteArray(Object obj) throws IOException { byte[] bytes = null; try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(obj); oos.flush(); bytes = bos.toByteArray(); } return bytes; } /** * Byte数组转对象 */ public static Object toObject(byte[] bytes) throws IOException, ClassNotFoundException { Object obj = null; try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bis)) { obj = ois.readObject(); } return obj; } }
package com.g2.order.server.vo; /** * CommonConst */ public class CommonConst { public static final String PREFIX = "g2:"; public static final String SETTING_PREFIX = PREFIX + "setting:"; /** * JSON_KEY */ public static final String SETTING_JSON_KEY = SETTING_PREFIX + "json:"; /** * LIST_KEY */ public static final String SETTING_LIST_KEY = SETTING_PREFIX + "list:"; /** * MAP_KEY */ public static final String SETTING_MAP_KEY = SETTING_PREFIX + "map:"; }
6.备注
这只是一个实现上的demo,如果要用到生产,可能还需要做以下改进
1.切面代码里写死了JedisCluster,这里要修改成一个接口 来支持单机/哨兵/集群 等
2.不支持毫秒级的存储(因为jedisCluster不支持...)
3.当没有获取缓存值时,应当根据key来加分布式锁,否则容易造成同样的处理多次执行