105

From the spring documentation :

@Cacheable(value="bookCache", key="isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

How can I specify @Cachable to use isbn and checkWarehouse as key?

phury
  • 2,123
  • 2
  • 21
  • 33

6 Answers6

118

Update: Current Spring cache implementation uses all method parameters as the cache key if not specified otherwise. If you want to use selected keys, refer to Arjan's answer which uses SpEL list {#isbn, #includeUsed} which is the simplest way to create unique keys.

From Spring Documentation

The default key generation strategy changed with the release of Spring 4.0. Earlier versions of Spring used a key generation strategy that, for multiple key parameters, only considered the hashCode() of parameters and not equals(); this could cause unexpected key collisions (see SPR-10237 for background). The new 'SimpleKeyGenerator' uses a compound key for such scenarios.

Before Spring 4.0

I suggest you to concat the values of the parameters in Spel expression with something like key="#checkWarehouse.toString() + #isbn.toString()"), I believe this should work as org.springframework.cache.interceptor.ExpressionEvaluator returns Object, which is later used as the key so you don't have to provide an int in your SPEL expression.

As for the hash code with a high collision probability - you can't use it as the key.

Someone in this thread has suggested to use T(java.util.Objects).hash(#p0,#p1, #p2) but it WILL NOT WORK and this approach is easy to break, for example I've used the data from SPR-9377 :

    System.out.println( Objects.hash("someisbn", new Integer(109), new Integer(434)));
    System.out.println( Objects.hash("someisbn", new Integer(110), new Integer(403)));

Both lines print -636517714 on my environment.

P.S. Actually in the reference documentation we have

@Cacheable(value="books", key="T(someType).hash(#isbn)") 
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

I think that this example is WRONG and misleading and should be removed from the documentation, as the keys should be unique.

P.P.S. also see https://jira.springsource.org/browse/SPR-9036 for some interesting ideas regarding the default key generation.

I'd like to add for the sake of correctness and as an entertaining mathematical/computer science fact that unlike built-in hash, using a secure cryptographic hash function like MD5 or SHA256, due to the properties of such function IS absolutely possible for this task, but to compute it every time may be too expensive, checkout for example Dan Boneh cryptography course to learn more.

Boris Treukhov
  • 17,493
  • 9
  • 70
  • 91
  • hmm i think spr-9036 ignores the situation where a parameter is an array, as arrays do not do deep equals by default – jasonk Nov 04 '13 at 07:08
  • Does this answer about key weekness actual for spring version `3.1.1`? [SPR-9377](https://jira.spring.io/browse/SPR-9377) was fixed for `3.1.1`, no? Could somebody add **UPDATED** section for this answer? – Cherry Aug 09 '16 at 07:15
  • Also does `T(someType).hash(#isbn)` work in Spring `3.1.1`? – Cherry Aug 09 '16 at 07:19
  • @Cherry Try using Arjan's answer ( `{ #root.methodName, #param1, #param2 }`, anyway 3.1.1 is old version now. – Boris Treukhov Aug 09 '16 at 13:26
  • Old, but I have to use it now :) So using Spring 3.2 is not sutable for me :( Yes there are plans for migration, but not right now. So actualization about this is important for me any other who still use old version. – Cherry Aug 09 '16 at 13:30
  • @Cherry are you sure that Arjan's solution is not working? If it's not, then you can just concat the string values anyway. – Boris Treukhov Aug 09 '16 at 15:49
  • how does it use all params, by toString? – Kalpesh Soni Oct 31 '17 at 16:21
  • @KalpeshSoni Arrays.deepEquals() - for objects in general it will end up with Object.equals(). Using toString() would add memory footprint and it's counterintuitive as every Object has equals() method in Java – Boris Treukhov Oct 31 '17 at 16:58
  • how will equals method generate a cache key? dont you have to first get an object out of the map? – Kalpesh Soni Oct 31 '17 at 18:04
99

After some limited testing with Spring 3.2, it seems one can use a SpEL list: {..., ..., ...}. This can also include null values. Spring passes the list as the key to the actual cache implementation. When using Ehcache, such will at some point invoke List#hashCode(), which takes all its items into account. (I am not sure if Ehcache only relies on the hash code.)

I use this for a shared cache, in which I include the method name in the key as well, which the Spring default key generator does not include. This way I can easily wipe the (single) cache, without (too much...) risking matching keys for different methods. Like:

@Cacheable(value="bookCache", 
  key="{ #root.methodName, #isbn?.id, #checkWarehouse }")
public Book findBook(ISBN isbn, boolean checkWarehouse) 
...

@Cacheable(value="bookCache", 
  key="{ #root.methodName, #asin, #checkWarehouse }")
public Book findBookByAmazonId(String asin, boolean checkWarehouse)
...

Of course, if many methods need this and you're always using all parameters for your key, then one can also define a custom key generator that includes the class and method name:

<cache:annotation-driven mode="..." key-generator="cacheKeyGenerator" />
<bean id="cacheKeyGenerator" class="net.example.cache.CacheKeyGenerator" />

...with:

public class CacheKeyGenerator 
  implements org.springframework.cache.interceptor.KeyGenerator {

    @Override
    public Object generate(final Object target, final Method method, 
      final Object... params) {

        final List<Object> key = new ArrayList<>();
        key.add(method.getDeclaringClass().getName());
        key.add(method.getName());

        for (final Object o : params) {
            key.add(o);
        }
        return key;
    }
}
Arjan
  • 22,808
  • 11
  • 61
  • 71
  • 1
    How can I get the custom KeyGenerator to be picked up in a xml-free configuration? – Basil Oct 21 '14 at 18:46
  • Thanks for pointing out to `{..., ..., ...}`. In the [current documentation](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html#cache-annotations-cacheable-default-key) it says that the default key generation will consider all parameters. So no need to create `CacheKeyGenerator`. – linqu May 25 '16 at 04:41
  • If this is not working for you, maybe your code does not know argument names, so use #a0 instead of #asin , a1 instead of #checkWarehouse etc. (also #p0, #p1). This happens for me in spring-data-jpa – Cipous Oct 04 '16 at 12:37
  • 2
    A _bit_ late, @linqu, but the default key generator does not include the method name. If one wants to use a single cache for multiple methods, then one needs to include the method name, if two methods can have the same parameters. – Arjan Jan 21 '17 at 22:58
  • A bit late for the first comment in this section (by @basil), but this page would help: https://www.baeldung.com/spring-cache-custom-keygenerator – moilejter Apr 23 '21 at 21:42
8

You can use a Spring-EL expression, for eg on JDK 1.7:

@Cacheable(value="bookCache", key="T(java.util.Objects).hash(#p0,#p1, #p2)")
Biju Kunjummen
  • 49,138
  • 14
  • 112
  • 125
  • And what about the collisions? P.S. Yes, this is what they tell to do in the reference, but the whole idea is very odd – Boris Treukhov Dec 29 '12 at 00:12
  • P.P.S. actually they *don't* tell to do in the reference - the only example with hashing is `@Cacheable(value="books", key="T(someType).hash(#isbn)") public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)` - but it seems to be a wrong and misleading example – Boris Treukhov Dec 29 '12 at 00:26
  • Actually I've found a counterexample with google (see my answer) – Boris Treukhov Dec 29 '12 at 01:28
  • @BorisTreukhov, the point was to show how arguments can be used to build a key using Spring-EL, not a solution which can be considered robust, I agree probably the simplest solution is to simply concatenate the arguments together which again can be done using Spring-EL. – Biju Kunjummen Dec 29 '12 at 14:13
  • 1
    I see, but the problem is not about which solution is simpler - the problem is that using hashcode is plain dangerous - if one client happened to get 109/434 keypair and another - 110/403 that may allow them for example to see each other's messages in the forum, or account operations in the internet-bank. I'm sure that there are a lot more collisions possible - good hash functions are hard to implement(consider looking at the source of md5 implementations - it's plain not multiplying on some magic number), and still they will never be able to return unique values. – Boris Treukhov Dec 29 '12 at 14:22
2

You can use Spring SimpleKey class

@Cacheable(value = "bookCache", key = "new org.springframework.cache.interceptor.SimpleKey(#isbn, #checkWarehouse)")
  • Does this keep increasing memory usage by filling heap causing memory leak as you are using `new` keyword here? – Sanjay May 10 '21 at 05:24
1

This will work

@Cacheable(value="bookCache", key="#checkwarehouse.toString().append(#isbn.toString())")
Madbreaks
  • 19,094
  • 7
  • 58
  • 72
Niraj Singh
  • 129
  • 4
  • I used spring-data-jpa and there has to be used a0 as argument on first position istead of using its name... – Cipous Oct 04 '16 at 12:35
-1

Use this

@Cacheable(value="bookCache", key="#isbn + '_' + #checkWarehouse + '_' + #includeUsed")
Gavy
  • 407
  • 3
  • 8
  • 1
    That combination is always unique, as your are just adding the string values combinations to key.. – Veswanth Jan 31 '19 at 07:23