如何在Spring接口中为@Cacheable注解绑定参数
我在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

