How to avoid case sensitive keys in Spring cache?

2.8k Views Asked by At

I'm using Spring caching (with EHCache) on server side defining the cache key(s) within @Cacheable. The problem is that different clients send the same strings that are used as keys with different spelling as they send it case sensitive. The result is that my caches contain more objects than they would have to.

Example: Let's say I have the following caching defined for a certain method:

@Cacheable(value = "myCache", key="{#myString}")
public SomeBusinessObject getFoo(String myString, int foo){
...
}

Now client A sends "abc" (all lowercase) to the Controller. Controller calls getFoo and "abc" is used as key to put an object into the cache. Client B sends "abC" (uppercase C) and instead of returning the cached object for key "abc" a new cache object for key "abC" is created.

How can I avoid the keys to be case sensitive?

I know I could define the cache key to be lowercase like this:

@Cacheable(value = "myCache", key="{#myString.toLowerCase()}")
public SomeBusinessObject getFoo(String myString, int foo){
...
}

This is of course working. But I'm looking for a more general solution. I have many caches and many cache keys and do some @CacheEvict(s) and @CachePut(s) and if I would use that "toLowerCase" approach I would always have to make sure not to forget it anywhere.

1

There are 1 best solutions below

0
On

As @gaston mentioned, the solution is replacing the default KeyGenerator. Implementing org.springframework.cache.annotation.CachingConfigurer or extending org.springframework.cache.annotation.CachingConfigurerSupport in your Configuration.

@Configuration
@EnableCaching
public class AppConfig extends CachingConfigurerSupport {
    @Override
    public KeyGenerator keyGenerator() {
        return new MyKeyGenerator();
    }

    @Bean
    @Override
    public CacheManager cacheManager() {
        //replaced with prefered CacheManager...
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("default")));
        return cacheManager;
    }
}

Here is a implementation modified from org.springframework.cache.interceptor.SimpleKeyGenerator.

import java.lang.reflect.Method;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKey;

public class MyKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        if (params.length == 0) {
            return SimpleKey.EMPTY;
        }
        if (params.length == 1) {
            Object param = params[0];
            if (param != null) {
                if (param.getClass().isArray()) {
                    return new MySimpleKey((Object[])param);
                } else {
                    if (param instanceof String) {
                        return ((String)param).toLowerCase();
                    }
                    return param;
                }
            }
        }
        return new MySimpleKey(params); 
    }
}

The original implementation produce key using SimpleKey class when @Cacheable method has more than one argument. Here is another implementation for producing case insensitive key.

import java.io.Serializable;
import java.util.Arrays;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils; 
@SuppressWarnings("serial")
public class MySimpleKey implements Serializable {
    private final Object[] params;
    private final int hashCode;

    /**
     * Create a new {@link SimpleKey} instance.
     * @param elements the elements of the key
     */
    public MySimpleKey(Object... elements) {
        Assert.notNull(elements, "Elements must not be null");
        Object[] lceles = new Object[elements.length];
        this.params = lceles;
        System.arraycopy(elements, 0, this.params, 0, elements.length);
        for (int i = 0; i < elements.length; i++) {
            Object o = elements[i];
            if (o instanceof String) {
                lceles[i] = ((String)o).toLowerCase();
            } else {
                lceles[i] = o;
            }
        }
        this.hashCode = Arrays.deepHashCode(lceles);
    }

    @Override
    public boolean equals(Object obj) {
        return (this == obj || (obj instanceof MySimpleKey
                && Arrays.deepEquals(this.params, ((MySimpleKey) obj).params)));
    }

    @Override
    public final int hashCode() {
        return this.hashCode;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
    }
}