0

I am trying to use Spring-JPA as shown in the below code.

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

Since interfaces do not have parameter names (unless compiled with debug info enabled), I am not able fetch the data because of the below exception.

 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)

Even if I use @Param or @P annotation, CacheOperationExpressionEvaluator is not able to resolve it since it uses DefaultParameterNameDiscoverer which internally uses StandardReflectionParameterNameDiscoverer and LocalVariableTableParameterNameDiscoverer. Had it been it would have also used AnnotationParameterNameDiscoverer, @Param would have been parsed.

What other solutions do we have to make this work without enabling compiler debug info or implementing EmployeeCrud interface?

Amit Daga
  • 127
  • 4
  • 13

1 Answers1

1

Easy! Without debug info enabled, you can refer to the parameter by position, such as this...

@Cacheable(cacheNames = "emp_by_last_name, key = "#a0")
List<Employee> findAllByLastName(@Param("lastName") String lastName);

See the Reference Documentation for more details.

Also, because the @Cacheable findAllByLastName(..) Repository method only has a single argument, then you do not need to explicitly call out the key to store the value (i.e. the method return value, in the case the List<Employee>) in the cache ("emp_by_last_name"), since by default, Spring's Cache Abstraction uses the method parameters of the cacheable method as the key. See here.

You should also be mindful of the fact that the cached result (i.e. the key/value) will be the entire List of Employee objects. The elements of the List will not be cached individually if that is what you are looking for. If you need this capability then refer to another post I have answered on more than 1 occassion.

Hope this helps.

Cheers! -John

John Blum
  • 7,381
  • 1
  • 20
  • 30
  • I am looking forward to a solution where I can instead use parameter name binding instead of using position like `a0` or `p0`. I understand that if I implement the interface, then parameter name binding will work but is it possible with interfaces? – Amit Daga May 25 '18 at 06:55
  • Why does it matter how you reference the key? The value of the key is the value of the method parameter anyway? Additionally, the `@Param` annotation is a Spring Data annotation. Why would the core Spring Framework Cache Abstraction know anything about the `@Param` annotation? At any rate, you could also... – John Blum May 25 '18 at 15:50
  • Implement your own (1 or more), custom `o.s.cache.interceptor.KeyGenerator(s)`, which would allow you to inspect the `@Caheable` method, perhaps search for `@Param` annotations on method parameters, etc. Spring's Cache Abstraction uses the `KeyGenerator` interface as the "strategy" to resolve the key in the cacheable operation. See here (https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java#L732-L738). As such, a `ParameterNameDiscoverer` of any kind is not going to help you. – John Blum May 25 '18 at 16:40
  • It does matter how we reference the key. Obviously the value would be the same but `#lastName` is any day more intuitive than `#p0`. Former is more maintainable than latter. – Amit Daga May 26 '18 at 06:53
  • You are right about KeyGenerator. But implementing key generator for all the keys is something I do not want. If `#lastName` is not possible in a simple way, then the next best solution is `#p0`. Also had it been that I was able to pass my own `ParameterNameDiscoverer`, then this problem would have been solved. Something like an extended custom parameter name discoverer and in that case I might have passed `AnnotationParameterNameDiscoverer` and there is nothing wrong about that I believe. – Amit Daga May 26 '18 at 06:55
  • Well, you don't need to implement a `KeyGenerator` for each key; that would be silly! You can write a reusable `KeyGenerator` that takes a constructor argument or arguments for all the `@Param` value(s) you want to use as your key for the cacheable operation. Then with that information and using reference to the cacheable `Method` in the `KeyGenerator.generate(..)` callback you can find the method parameters annotated with desired `@Param` annotations and use their values as the key in the cache operation. ... – John Blum May 26 '18 at 21:30
  • Then you only need declare beans for all the different key combinations using the same `KeyGenerator` class where caching is needed in your application. If you are not happy with this solution, then I suggest you contribute a PR. But, you need to abstract enough of the different types in play since, clearly, the Spring Framework's Cache Abstraction cannot know anything about `@Param`. I leave you at this. – John Blum May 26 '18 at 21:31
  • Thanks John Blum for the help. Will check for the best solution. – Amit Daga May 28 '18 at 08:27