redis緩存切面實現(支持緩存key的spel表達式)

1.定義註解java

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.定義切面web

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.引用代碼redis

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的返回結構)spring

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.輔助代碼express

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.備註json

 

這只是一個實現上的demo,若是要用到生產,可能還須要作如下改進數組

1.切面代碼裏寫死了JedisCluster,這裏要修改爲一個接口 來支持單機/哨兵/集羣 等緩存

2.不支持毫秒級的存儲(由於jedisCluster不支持...)app

3.當沒有獲取緩存值時,應當根據key來加分佈式鎖,不然容易形成一樣的處理屢次執行分佈式

相關文章
相關標籤/搜索