写点什么

自定义 aop 实现 Cacheable 注解 (零拷贝), CacheItemGet,CacheMapGet,CacheMapPut

用户头像
张音乐
关注
发布于: 4 小时前

一.开发背景

众所周知, 在 SpringBoot 中有一个管理 Redis 缓存的注解 Cacheable, 可以很方便的获取缓存

但是针对 Cacheable 的痛点,缓存的数据在客户端中查看,如果遇到错误很难排查错误。并且 Cacheable 会将整个数据获取到 JVM 内存中, 会对应用系统造成负担.如果数据量很大的话, 还可能会造成内存溢出问题.

Cacheable 不方便使用的点

针对 Map 类型做处理,并且 Redis 中也想存储成 Map 集合。

针对 Redis 的 Set 集合也可以开发类似的组件

二.实现思路

使用 aop 来实现, 在切面中在执行目标方法时先执行对缓存的操作,然后再判断是否执行目标方法.

对缓存的操作, 按照自己的设计来实现, 比如, 返回值是一个 Map 类型的化, 执行对 Map 的操作, 如果返回值是一个 List 的话, 执行对 List 的操作.

另外, 我们还需要对参数进行解析和替换, 例如, 我们可能需要动态的去指定缓存的 Key, 或者 Map 中的 Key 来根据参数进行生成,这个时候我们需要动态进行参数绑定,

还有一个需要注意的点是, 对缓存进行 PUT 操作时, 需要加锁, 防止缓存击穿或者缓存穿透现象的发生.

三.具体实现

1、定义注解

1)、获取 map 中某一项值的注解

这个注解用于获取指定 redis key 下指定 Map key 的数据

package com.biubiu.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CacheItemGet {    String key();    String hKey();}
复制代码

2)、获取 map 集合的注解

这个注解用于获取指定 redis key 下的整个 Map 数据, 返回的是 Map 结构

package com.biubiu.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CacheMapGet {    String key();    long expire() default 12 * 60 * 60L;    boolean parse() default false;}
复制代码

3)、更新 map 的注解 

package com.biubiu.annotation; import java.lang.annotation.*; @Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface CacheMapPut {     String key();    long expire() default 12 * 60 * 60L;    boolean parse() default false;}
复制代码

2、redisTemplate

package com.biubiu;   @Configurationpublic class Config extends WebMvcConfigurationSupport {      @Bean(name = "hashRedisTemplate")    public RedisTemplate<String, Object> hashRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {        RedisTemplate<String, Object> template = new RedisTemplate<>();        template.setConnectionFactory(lettuceConnectionFactory);        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);        ObjectMapper om = new ObjectMapper();        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        jackson2JsonRedisSerializer.setObjectMapper(om);        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();        // key采用String的序列化方式        template.setKeySerializer(stringRedisSerializer);        // hash的key也采用String的序列化方式        template.setHashKeySerializer(stringRedisSerializer);        // value序列化方式采用jackson        template.setValueSerializer(jackson2JsonRedisSerializer);        // hash的value序列化方式采用jackson        template.setHashValueSerializer(jackson2JsonRedisSerializer);        template.afterPropertiesSet();        return template;    }    }
复制代码

3、aop 切面

获取 Map 中某一项数据的切面

package com.biubiu.aop; import com.biubiu.annotation.CacheItemGet;import com.biubiu.util.CommonUtil;import com.biubiu.util.MD5;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.core.DefaultParameterNameDiscoverer;import org.springframework.data.redis.core.HashOperations;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.expression.EvaluationContext;import org.springframework.expression.Expression;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 javax.annotation.Resource;import java.lang.reflect.Method; @Aspect@Componentpublic class CacheItemGetAspect {     @Qualifier("hashRedisTemplate")    @Resource    private RedisTemplate<String, Object> redisTemplate;     @Around("@annotation(com.biubiu.annotation.CacheItemGet)")    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());        CacheItemGet cacheItemGet = method.getAnnotation(CacheItemGet.class);        String key = cacheItemGet.key();        String hKeyEl = cacheItemGet.hKey();        //创建解析器        ExpressionParser parser = new SpelExpressionParser();        Expression hKeyExpression = parser.parseExpression(hKeyEl);        //设置解析上下文有哪些占位符。        EvaluationContext context = new StandardEvaluationContext();        //获取方法参数        Object[] args = joinPoint.getArgs();        String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(method);        for(int i = 0; i < parameterNames.length; i++) {            context.setVariable(parameterNames[i], args[i]);        }        //解析得到 item的 key        String hKeyValue = hKeyExpression.getValue(context).toString();        String hKey = MD5.getMD5Str(hKeyValue);        HashOperations<String, String, Object> ops = redisTemplate.opsForHash();        Object value = ops.get(key, hKey);        if(value != null) {            return value.toString();        }        return joinPoint.proceed();    } }
复制代码

获取整个 Map 结构的切面

package com.biubiu.aop; import com.biubiu.annotation.CacheMapGet;import com.biubiu.annotation.CacheMapPut;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.data.redis.core.HashOperations;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component; import javax.annotation.Resource;import java.lang.reflect.Method;import java.util.Map;import java.util.concurrent.TimeUnit; @Aspect@Componentpublic class CacheMapGetAspect {    @Qualifier("hashRedisTemplate")    @Resource    private RedisTemplate<String, Object> redisTemplate;     @Around("@annotation(com.biubiu.annotation.CacheMapGet)")    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());        CacheMapGet cacheMap = method.getAnnotation(CacheMapGet.class);        String key = cacheMap.key();        //强制刷缓存        HashOperations<String, String, Object> ops = redisTemplate.opsForHash();        Object val;        Map<String, Object> value = ops.entries(key);        if(value.size() != 0) {            return value;        }        //加锁        synchronized (this) {            value = ops.entries(key);            if(value.size() != 0) {                return value;            }            //执行目标方法            val = joinPoint.proceed();            ops.delete(key);            //把值设置回去            ops.putAll(key, (Map<String, Object>) val);            redisTemplate.expire(key, cacheMap.expire(), TimeUnit.SECONDS);            return val;        }    } }
复制代码

更新缓存的切面

package com.biubiu.aop; import com.biubiu.annotation.CacheMapPut;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.data.redis.core.HashOperations;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component; import javax.annotation.Resource;import java.lang.reflect.Method;import java.util.Map;import java.util.concurrent.TimeUnit; @Aspect@Componentpublic class CacheMapPutAspect {     @Qualifier("hashRedisTemplate")    @Resource    private RedisTemplate<String, Object> redisTemplate;     @Around("@annotation(com.biubiu.annotation.CacheMapPut)")    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {        MethodSignature signature = (MethodSignature) joinPoint.getSignature();        Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());        CacheMapPut cacheMap = method.getAnnotation(CacheMapPut.class);        String key = cacheMap.key();        //强制刷缓存        HashOperations<String, String, Object> ops = redisTemplate.opsForHash();        Object val;        //加锁        synchronized (this) {            //执行目标方法            val = joinPoint.proceed();            redisTemplate.delete(key);            //把值设置回去            ops.putAll(key, (Map<String, Object>) val);            redisTemplate.expire(key, cacheMap.expire(), TimeUnit.SECONDS);            return val;        }    } }
复制代码

4、使用方法

        @CacheItemGet(key = "biubiu-field-hash", hKey = "#tableName+#colName")    public String getValue(String tableName, String colName) {                //省略...    }
复制代码


    @CacheMapGet(key = "biubiu-field-hash", expire = 6 * 60 * 60L)    public Map<String, String> getFieldTypeMap() {                //省略...    }
复制代码


    @CacheMapPut(key = "biubiu-field-hash", expire = 6 * 60 * 60L)    public Map<String, String> putFieldTypeMap() {        //省略...    }
复制代码


5、MD5 工具类

package com.biubiu.util; import java.nio.charset.Charset;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException; import org.apache.log4j.LogManager;import org.apache.log4j.Logger;  /** * @Author yule.zhang * @CreateTime 2019-6-16下午05:28:11 * @Version 1.0 * @Explanation 用MD5对数据进行加密 */public class MD5 { 	static final Logger log = LogManager.getLogger(MD5.class); 	MessageDigest md5; 	static final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };	public MD5() {		try {			// 获得MD5摘要算法的 MessageDigest 对象			md5 = MessageDigest.getInstance("MD5");		} catch (NoSuchAlgorithmException e) {			log.error("创建MD5对象出错, ", e);			throw new IllegalArgumentException("创建md5对象时出错");		}	}	public synchronized String getMD5(String s) {		return this.getMD5(s.getBytes()).toLowerCase();	}	public synchronized String getMD5(byte[] btInput) {		try {			// 使用指定的字节更新摘要			md5.update(btInput);			// 获得密文			byte[] md = md5.digest();			// 把密文转换成十六进制的字符串形式			int j = md.length;			char str[] = new char[j * 2];			int k = 0;			for (int i = 0; i < j; i++) {				byte byte0 = md[i];				str[k++] = hexDigits[byte0 >>> 4 & 0xf];				str[k++] = hexDigits[byte0 & 0xf];			}			return new String(str).toLowerCase();		} catch (Exception e) {			log.error("生成MD5码时出错,", e);			throw new IllegalArgumentException("生成MD5出错");		}	}		 /**	  * 获取32位的MD5加密	  * @param sourceStr	  * @return	  */	public static String getMD5Str(String sourceStr) {	        String result = "";	        try {	            MessageDigest md = MessageDigest.getInstance("MD5");	            md.update(sourceStr.getBytes(Charset.forName("utf-8")));	            byte b[] = md.digest();	            int i;	            StringBuffer buf = new StringBuffer("");	            for (int offset = 0; offset < b.length; offset++) {	                i = b[offset];	                if (i < 0)	                    i += 256;	                if (i < 16)	                    buf.append("0");	                buf.append(Integer.toHexString(i));	            }	            result = buf.toString();	        } catch (NoSuchAlgorithmException e) {	            System.out.println(e);	        }	        return result;   }}
复制代码


发布于: 4 小时前阅读数: 2
用户头像

张音乐

关注

求你关注我,别不识抬举.别逼我跪下来求你. 2021.03.28 加入

还未添加个人简介

评论

发布
暂无评论
自定义aop实现Cacheable注解(零拷贝), CacheItemGet,CacheMapGet,CacheMapPut