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來加分佈式鎖,否則容易造成同樣的處理多次執行

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章