You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring接口中为@Cacheable注解绑定参数

问题:Spring JPA中@Cacheable结合@Param无法解析参数(不开启编译调试信息/不实现接口)

我在Spring JPA里尝试用这段代码实现按姓氏缓存员工查询:

@Repository 
public interface EmployeeCrud extends CrudRepository<Employee, Integer> { 
    @Cacheable(cacheNames = "emp_by_last_name", key = "#lastName") 
    List<Employee> findAllByLastName(@Param("lastName") String lastName); 
}

但因为接口默认不包含参数名称(除非开启编译调试信息),导致缓存key生成失败,抛出了如下异常:

java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?) Builder[public abstract java.util.List EmployeeCrud.findAllByLastName(java.lang.String)] caches=[emp_by_last_name] | key='#lastName' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 
at org.springframework.cache.interceptor.CacheAspectSupport.generateKey(CacheAspectSupport.java:561) 
at org.springframework.cache.interceptor.CacheAspectSupport.findCachedItem(CacheAspectSupport.java:502) 
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:389) 
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:327) 
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:61) 
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

我试过用@Param或@P注解,但Spring的CacheOperationExpressionEvaluator依赖的DefaultParameterNameDiscoverer只支持StandardReflectionParameterNameDiscoverer和LocalVariableTableParameterNameDiscoverer,并不支持通过@Param注解解析参数名称,所以问题依然存在。

想请教各位:在不开启编译调试信息,也不手动实现EmployeeCrud接口的前提下,还有哪些可行的解决方案?


可行解决方案

1. 使用参数索引生成缓存key(最快捷)

既然没法通过参数名称获取值,我们可以直接利用参数的位置索引来指定缓存key。Spring的SpEL支持通过#root.args[索引]来访问方法参数,这里第一个参数的索引是0,修改后的代码如下:

@Repository 
public interface EmployeeCrud extends CrudRepository<Employee, Integer> { 
    @Cacheable(cacheNames = "emp_by_last_name", key = "#root.args[0]") 
    List<Employee> findAllByLastName(@Param("lastName") String lastName); 
}

这样Spring缓存模块就能直接从参数数组中取到对应的值,完全不需要依赖参数名称信息,简单直接。

2. 自定义支持@Param的KeyGenerator

如果希望继续使用参数名称(比如lastName)来生成key,我们可以自定义一个KeyGenerator,让它能解析@Param注解里的参数名称。具体步骤如下:

第一步:实现自定义KeyGenerator

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.data.repository.query.Param;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

public class ParamAnnotationKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        Map<String, Object> paramMap = new HashMap<>();
        Parameter[] parameters = method.getParameters();
        
        // 遍历方法参数,解析@Param注解
        for (int i = 0; i < parameters.length; i++) {
            Param paramAnnotation = parameters[i].getAnnotation(Param.class);
            if (paramAnnotation != null) {
                paramMap.put(paramAnnotation.value(), params[i]);
            } else {
                // 没有@Param的情况, fallback到参数索引
                paramMap.put("arg" + i, params[i]);
            }
        }
        
        // 这里直接返回我们需要的lastName作为key,也可以根据需求组合多个参数
        return paramMap.get("lastName");
    }
}

第二步:注册自定义KeyGenerator到Spring容器

在你的缓存配置类里,把这个自定义的KeyGenerator注册为Bean:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfiguration {
    @Bean("paramAnnotationKeyGenerator")
    public KeyGenerator paramAnnotationKeyGenerator() {
        return new ParamAnnotationKeyGenerator();
    }
}

第三步:在@Cacheable中指定使用这个KeyGenerator

修改Repository接口的注解,指定keyGenerator为我们注册的Bean名称:

@Repository 
public interface EmployeeCrud extends CrudRepository<Employee, Integer> { 
    @Cacheable(cacheNames = "emp_by_last_name", keyGenerator = "paramAnnotationKeyGenerator") 
    List<Employee> findAllByLastName(@Param("lastName") String lastName); 
}

这样自定义的KeyGenerator就会解析@Param注解的参数名称,生成对应的缓存key,完全不需要编译调试信息。

3. 用SpEL直接解析@Param注解(略显冗长)

我们也可以在SpEL表达式里直接解析参数上的@Param注解,虽然写法比较长,但也是一种可行方案:

@Repository 
public interface EmployeeCrud extends CrudRepository<Employee, Integer> { 
    @Cacheable(
        cacheNames = "emp_by_last_name", 
        key = "#root.method.parameters[0].getAnnotation(T(org.springframework.data.repository.query.Param)).value() + '_' + #root.args[0]"
    ) 
    List<Employee> findAllByLastName(@Param("lastName") String lastName); 
}

这个表达式会先获取第一个参数上的@Param注解的值,再拼接参数值作为缓存key,不过可读性不如前两种方案,适合特殊场景下使用。


内容的提问来源于stack exchange,提问作者Amit Daga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 10:10:13