高并发|Spring自定义注解实现redis缓存

一、前言
redis是分布式微服务中必用的基础组件之一,现在国内的大部分项目基本上用到,缓存是其主要作用之一,而在项目中频繁使用set()方法添加注解,会造成代码的重复和臃肿,对于开发经验不足的小白,甚至会因为缓存的添加不当直接影响到正常的业务流程,从而酿成事故,因此成熟的公司都会通过封装基础组件,实现通过注解自动添加redis缓存,本文会从原理出发,带领大家亲自实现自定义注解,完成redis缓存的开发,学会了,你可以在同事面前秀一把了。
二、自定义注解的参数说明
@Target: 注解的作用目标,即注解可以使用的位置,通常有
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention 注解的保留位置 用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicy,RetentionPolicy有三种策略,分别是:
SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。
@Documented 注解只是用来做标识,没什么实际作用,了解就好。
如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。
三、自定义redis注解
JhRedisCache——添加redis缓存的注解

/** * @author ljx * @Description: 添加redis缓存的注解 * @date 2020/6/9 4:11 下午 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface JhRedisCache { /* * redis缓存中的key,支持spel表达式 */ String key() default ""; /* * 缓存时间,默认缓存时间是一天 60*60*24 */ long expire() default 86400L; /* * 如果注解添加在返回list的方法上,则需要通过该字段指定list中的class类型 */ Class type() default Object.class; }

JhRedisCacheEvict——删除redis缓存的注解
/** * @author ljx * @Description: 删除redis缓存的注解 * @date 2020/6/10 3:38 下午 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface JhRedisCacheEvict { String key() default ""; }

四、自定义AOP切面
/** * @author ljx * @Description: 注解切面 * @date 2020/6/9 11:12 上午 */ @Component @Aspect public class RedisCacheAspect { private static final String SPEL = "#"; private static final String KEY_SEPARATOR = "_"; private static final int TWO = 2; private static final Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class); private RedisClient redisClient; private AppInfo appInfo; /** * @Description: 在使用JhRedisCache注解的地方切入此切点,查询缓存是否在redis中存在,若已存在,则直接返回,否则查询数据库 * @param pjp 切入点信息 * @return java.lang.Object 方法返回值 * @Author: ljx * @Date: 2020/6/9 4:20 下午 */ @Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCache)") private Object handleCache(final ProceedingJoinPoint pjp) throws Throwable { // 获取切入的方法对象 // 这个m是代理对象的,没有包含注解 Method m = ((MethodSignature) pjp.getSignature()).getMethod(); // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解 Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes()); // 根据目标方法对象获取注解对象 JhRedisCache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCache.class); // 解析key String keyExpr = cacheAnnotation.key(); Object[] as = pjp.getArgs(); String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as); // 到redis中获取缓存 String cache = null; try { cache = redisClient.get(key); } catch (Exception e) { logger.error("{}查询redis缓存异常:{}",keyExpr,e.getMessage()); } if (StringUtils.isBlank(cache)) { // 若不存在,则到数据库中去获取 Object result = pjp.proceed(); // 从数据库获取后存入redis,若有指定过期时间,则设置 try { long expireTime = cacheAnnotation.expire(); if (expireTime > 0) { redisClient.set(key,JSON.toJSONString(result), expireTime, TimeUnit.SECONDS); }else{ redisClient.set(key, JSON.toJSONString(result)); } } catch (Exception e) { logger.warn("{}{}缓存redis异常:{}",keyExpr,e.getMessage(),result); } return result; } // 得到被代理的方法上的注解 Class modelType = cacheAnnotation.type(); // 得到被代理方法的返回值类型 Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType(); // 返回反序列化从缓存中拿到的json return deserialize(cache, returnType, modelType); }@Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCacheEvict)") private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable { // 获取切入的方法对象 // 这个m是代理对象的,没有包含注解 Method m = ((MethodSignature) pjp.getSignature()).getMethod(); // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解 Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes()); // 根据目标方法对象获取注解对象 JhRedisCacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCacheEvict.class); // 解析key String keyExpr = cacheEvictAnnotation.key(); Object[] as = pjp.getArgs(); String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as); // 先删除数据库中的用户信息再删除缓存 Object result = pjp.proceed(); redisClient.delete(key); return result; }public RedisCacheAspect() { // 初始化redisClient对象,不同的项目可能实现不同,此处是结合自己项目中的redis实现的 appInfo= SpringContext.getBean(AppInfo.class); String appName = appInfo.getAppName(); this.redisClient = SpringContext.getBean(appName, RedisClient.class); }/** * @Description: 解析注解中的key,支持spel表达式的解析 * @param spelExpress 注解中spel表达式 * @param method 方法对象 * @param params 方法参数 * @return java.lang.String * @Author: ljx * @Date: 2020/6/10 3:16 下午 */ private String getRedisKeyBySpel(String spelExpress, Method method, Object[] params) { String redisKey = appInfo.getAppName()+KEY_SEPARATOR+method.getName(); // 如果为空,则默认服务名_方法名 if (StringUtils.isBlank(spelExpress)){ return redisKey; } // 如果不是spel表达式,则直接使用用户传入的key if(!spelExpress.contains(SPEL)){ return spelExpress; } // 如果是spel表达式,但是参数为空,则默认服务名_方法名 if(params==null){ return redisKey; } ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); // spel表达式用到的变量,设置第一个参数 context.setVariable("entity", params[0]); // 设置第二个参数 if(params.length>1&¶ms[1]!=null){ context.setVariable("entityTwo", params[1]); } // 设置第三个参数 if(params.length>TWO&¶ms[TWO]!=null){ context.setVariable("entityTrd", params[2]); } // 解析spel表达式 Expression expression = parser.parseExpression(spelExpress, new TemplateParserContext()); final Object value = https://www.it610.com/article/expression.getValue(context); return redisKey + KEY_SEPARATOR+"_"+Objects.toString(value,""); }/** * @Description: FastJSON反序列化获得对象 * @param json 从redis缓存中获取的字符串 * @param clazz 添加注解的方法返回值的class类型 * @param modelType 转换成list中的class类型 * @return java.lang.Object * @Author: ljx * @Date: 2020/6/11 3:50 下午 */ private Object deserialize(String json, Class clazz, Class modelType) { return clazz.isAssignableFrom(List.class) ? JSON.parseArray(json, modelType) : JSON.parseObject(json, clazz); }}

五、使用案例
【高并发|Spring自定义注解实现redis缓存】简单的使用:
@JhRedisCache(key = "#{#entity}") public TeachCenter selectTeacherCenter(Integer teachCenterId) { return teachCenterMapper.selectByPrimaryKey(teachCenterId); }

复杂使用:
@JhRedisCache(key = "#{#entity.getProvinceId()}", type = TeachCenter.class) public List selectTeachCenterList(TeachCenterCommonRequest teacherCenterCommonRequest) { //获取地区id; Integer provinceId = teacherCenterCommonRequest.getProvinceId(); //分页 PageHelper.startPage(teacherCenterCommonRequest.getPageNum(), teacherCenterCommonRequest.getPageSize()); //获取所有教学中心 List teachCenterList = teachCenterMapper.selectByProvinceId(provinceId); return teachCenterList; }

本人亲测有效,并已在公司项目中大规模使用,因为依赖redis的配置,这里不再带领大家测试,有兴趣的小伙伴可以在项目中测试看看,欢迎有问题随时沟通。

    推荐阅读