AOP + SpEL 自定义注解实现自动缓存机制

✨背景

在实际开发中,我们经常会给某些接口加缓存,来避免重复查询数据库,提高系统性能。Spring 自带的 @Cacheable 虽然功能强大,但在灵活性上有一定限制,比如:

  • 想要自定义缓存 key 生成逻辑?

  • 想按参数任意字段组合缓存 key?

  • 想自由设置缓存时间?

  • 想要缓存所有接口结果(甚至自定义统一返回结构)?

于是,我基于 AOP + Redis + SpEL 表达式,自定义了一个 @CustomCache 注解,完美解决这些问题。

🧩功能亮点

  • ✅ 支持 SpEL 表达式动态生成缓存 key;

  • ✅ 支持基于参数内容自动构造缓存 key;

  • ✅ 支持缓存时间控制;

  • ✅ 支持缓存数据反序列化为方法真实返回值类型(包括泛型);

📦注解定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomCache {
    String key() default "";     // 支持 SpEL 表达式
    int expire() default 60;     // 缓存有效期,单位秒
}

🧠切面逻辑核心实现

@Aspect
@Component
@RequiredArgsConstructor
public class CustomCacheAspect {

    private final RedisTemplate<String, Object> redisTemplate;

    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Around("@annotation(customCache)")
    public Object around(ProceedingJoinPoint joinPoint, CustomCache customCache) throws Throwable {
        String key = generateCacheKey(joinPoint, customCache);

        String cached = ConvertUtil.getValue(redisTemplate.opsForValue().get(key), "");
        if (StringUtils.isNotEmpty(cached)) {
            // 从缓存中取出数据并转化为方法的返回类型
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            ResolvableType returnType = ResolvableType.forMethodReturnType(method);
            JavaType javaType = objectMapper.getTypeFactory().constructType(returnType.getType());
            return objectMapper.readValue(cached, javaType);
        }

        Object result = joinPoint.proceed();
        redisTemplate.opsForValue().set(key, JsonUtil.toJsonString(result), customCache.expire(), TimeUnit.SECONDS);
        return result;
    }

    /**
     * 生成缓存 Key:优先解析 SpEL,如果没有设置则使用默认 key 生成规则(类名.方法名 + 参数 JSON 的 MD5)
     */
    private String generateCacheKey(ProceedingJoinPoint pjp, CustomCache customCache) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Object[] args = pjp.getArgs();
        String[] paramNames = nameDiscoverer.getParameterNames(method);

        // 1. 用户设置了 SpEL 表达式,优先使用 SpEL 解析
        if (!customCache.key().isEmpty()) {
            EvaluationContext context = new StandardEvaluationContext();
            for (int i = 0; i < args.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            try {
                String spelKey = parser.parseExpression(customCache.key()).getValue(context, String.class);
                return "cache:" + spelKey;
            } catch (Exception e) {
                throw new IllegalArgumentException("SpEL 表达式解析失败: " + customCache.key(), e);
            }
        }

        // 2. 没有 SpEL,使用类名+方法名+参数 JSON 的 MD5 作为 key
        String className = pjp.getTarget().getClass().getName();
        String methodName = method.getName();

        try {
            // 把参数序列化成 JSON 字符串
            String paramsJson = objectMapper.writeValueAsString(args);
            // 对 JSON 字符串做 MD5 摘要
            String paramsHash = DigestUtils.md5Hex(paramsJson);

            return "cache:" + className + "." + methodName + ":" + paramsHash;
        } catch (Exception e) {
            // 如果序列化失败,退回到使用 toString 拼接(不推荐,但保证不中断)
            String argsKey = Arrays.stream(args)
                    .map(arg -> arg == null ? "null" : arg.toString())
                    .collect(Collectors.joining(":"));
            return "cache:" + className + "." + methodName + ":" + argsKey;
        }
    }
}

🧪使用方式示例

DTO 示例:

@Data
public class UserDTO {
    private Long id;
    private String name;
}

接口使用示例:

    @PostMapping("create")
    @CustomCache(key = "#user.id + ':' + #user.name", expire = 60)// 使用SpEL
    public CustomJsonResult createUser(@Valid @RequestBody UserDTO user) {
        // 业务逻辑…
        return CustomJsonResult.success(user);
    }

    @PostMapping("create2")
    @CustomCache(expire = 60) // 默认key为方法参数
    public CustomJsonResult createUser2(@Valid @RequestBody UserDTO user) {
        // 业务逻辑…
        return CustomJsonResult.success(user);
    }

❓常见问题

❌ 报错:InvalidDefinitionException: Cannot construct instance of …

原因:Jackson 反序列化 CustomJsonResult 等类时需要默认构造方法。

✅ 解决:

  • 给类加 @NoArgsConstructor

  • 或使用 @JsonCreator + @JsonProperty 支持构造方式反序列化

 
posted @ 2025-05-19 15:04  ~落辰~  阅读(57)  评论(0)    收藏  举报