Redis+自定义注解+AOP实现声明式注解缓存查询的示例

2025-04-02 20:19:01 102
魁首哥

引言:为什么需要声明式缓存?

  • 背景痛点:传统代码中缓存逻辑与业务逻辑高度耦合,存在重复代码、维护困难等问题(如手动判断缓存存在性、序列化/反序列化操作)
  • 解决方案:通过注解+aop实现缓存逻辑与业务解耦,开发者只需关注业务,通过注解配置缓存策略(如过期时间、防击穿机制等)
  • 技术价值:提升代码可读性、降低维护成本、支持动态缓存策略扩展。

核心流程设计

方法调用 → 切面拦截 → 生成缓存key → 查询redis → 
└ 命中 → 直接返回缓存数据
└ 未命中 → 加锁查db → 结果写入redis → 返回数据

二、核心实现步骤

1. 定义自定义缓存注解(如@rediscache)

package com.mixchains.ytboot.common.annotation;

import java.lang.annotation.elementtype;
import java.lang.annotation.retention;
import java.lang.annotation.retentionpolicy;
import java.lang.annotation.target;
import java.util.concurrent.timeunit;

/**
 * @author 卫相yang
 * oversion03
 */
@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface rediscache {
    /**
     * redis键前缀(支持spel表达式)
     */
    string key();

    /**
     * 过期时间(默认1天)
     */
    long expire() default 1;

    /**
     * 时间单位(默认天)
     */
    timeunit timeunit() default timeunit.days;

    /**
     * 是否缓存空值(防穿透)
     */
    boolean cachenull() default true;
}

2. 编写aop切面(核心逻辑)

切面职责

  • 缓存key生成:拼接类名、方法名、参数哈希(md5或spel动态参数)本次使用的是spel
  • 缓存查询:优先从redis读取,使用fastjson等工具反序列化

空值缓存:缓存null值并设置短过期时间,防止恶意攻击

package com.mixchains.ytboot.common.aspect;

import com.alibaba.fastjson.json;
import com.mixchains.ytboot.common.annotation.rediscache;
import io.micrometer.core.instrument.util.stringutils;
import lombok.extern.slf4j.slf4j;
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.autowired;
import org.springframework.core.defaultparameternamediscoverer;
import org.springframework.core.parameternamediscoverer;
import org.springframework.data.redis.core.redistemplate;
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 java.lang.reflect.type;

/**
 * @author 卫相yang
 * oversion03
 */
@aspect
@component
@slf4j
public class rediscacheaspect {
    @autowired
    private redistemplate redistemplate;

    private final expressionparser parser = new spelexpressionparser();
    private final parameternamediscoverer parameternamediscoverer = new defaultparameternamediscoverer();

    @around("@annotation(rediscache)")
    public object around(proceedingjoinpoint joinpoint, rediscache rediscache) throws throwable {
        method method = ((methodsignature) joinpoint.getsignature()).getmethod();
        // 解析spel表达式生成完整key
        string key = parsekey(rediscache.key(), method, joinpoint.getargs());
        // 尝试从缓存获取
        string cachedvalue = redistemplate.opsforvalue().get(key);
        if (stringutils.isnotblank(cachedvalue)) {
            type returntype = ((methodsignature) joinpoint.getsignature()).getreturntype();
            return json.parseobject(cachedvalue, returntype);
        }
        // 执行原方法
        object result = joinpoint.proceed();
        // 处理缓存存储
        if (result != null || rediscache.cachenull()) {
            string valuetocache = result != null ?
                    json.tojsonstring(result) :
                    (rediscache.cachenull() ? "[]" : null);

            if (valuetocache != null) {
                redistemplate.opsforvalue().set(
                        key,
                        valuetocache,
                        rediscache.expire(),
                        rediscache.timeunit()
                );
            }
        }
        return result;
    }

    private string parsekey(string keytemplate, method method, object[] args) {
        string[] paramnames = parameternamediscoverer.getparameternames(method);
        evaluationcontext context = new standardevaluationcontext();
        if (paramnames != null) {
            for (int i = 0; i < paramnames.length; i++) {
                context.setvariable(paramnames[i], args[i]);
            }
        }
        return parser.parseexpression(keytemplate).getvalue(context, string.class);
    }
}

代码片段示例

 @rediscache(
            key = "'category:homesecond:' + #categorytype",  //缓存的key + 动态参数
            expire = 1, //过期时间
            timeunit = timeunit.days // 时间单位
    )
    @override
    public returnvo> listhomesecondgoodscategory(integer level, integer categorytype) {
        // 数据库查询
        list dblist = goodscategorymapper.selectlist(
                new lambdaquerywrapper()
                        .eq(goodscategory::getcategorylevel, level)
                        .eq(goodscategory::getcategorytype, categorytype)
                        .eq(goodscategory::getishomepage, 1)
                        .orderbydesc(goodscategory::gethomesort)
        );
        // 设置父级uuid(可优化为批量查询)
        list parentids = dblist.stream().map(goodscategory::getparentid).distinct().collect(collectors.tolist());
        map parentmap = goodscategorymapper.selectbatchids(parentids)
                .stream()
                .collect(collectors.tomap(goodscategory::getid, goodscategory::getuuid));

        dblist.foreach(item -> item.setparentuuid(parentmap.get(item.getparentid())));
        return returnvo.ok("列出首页二级分类", dblist);
    }

最终效果:

到此这篇关于redis+自定义注解+aop实现声明式注解缓存查询的示例的文章就介绍到这了,更多相关redis 声明式注解缓存查询内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

分享
海报
102
上一篇:Mysql什么情况下不会命中索引 下一篇:Pandas使用SQLite3实战

忘记密码?

图形验证码