Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Cache not working for abstract classes

I'm trying to use Spring Cache within abstract classes but it won't work, because, from what I can see, Spring is searching for CacheNames on the abstract class. I'm having a REST API which uses a service layer and a dao layer. The idea is to have a different cache name for every subclass.

My abstract service class looks like this:

    @Service
    @Transactional
    public abstract class AbstractService<E> {

...

    @Cacheable
    public List<E> findAll() {
        return getDao().findAll();
    }
}

An extension of the abstract class would look like this:

@Service
@CacheConfig(cacheNames = "textdocuments")
public class TextdocumentsService extends AbstractService<Textdocuments> {
...
}

So when I start the application with this code, Spring gives me the following exception:

Caused by: java.lang.IllegalStateException: No cache names could be detected on 'public java.util.List foo.bar.AbstractService.findAll()'. Make sure to set the value parameter on the annotation or declare a @CacheConfig at the class-level with the default cache name(s) to use.
    at org.springframework.cache.annotation.SpringCacheAnnotationParser.validateCacheOperation(SpringCacheAnnotationParser.java:240) ~[spring-context-4.1.6.RELEASE.jar:?]

I think this happens because Spring is searching for the CacheName on the abstract class, despite it is being declared on the subclass.

Trying to use

 @Service
 @Transactional
 @CacheConfig
        public abstract class AbstractService<E> {
    }

leads to the same exception; using

 @Service
 @Transactional
 @CacheConfig(cacheNames = "abstractservice")
        public abstract class AbstractService<E> {
    }

gives no exception, but then Spring Cache uses the same cache name for every subclass and ignores the cache name defined on the subclass. Any Ideas to so solve this?

like image 611
Matthias Avatar asked May 02 '16 07:05

Matthias


People also ask

What is Spring cache abstraction?

Just like other services in the Spring Framework, the caching service is an abstraction (not a cache implementation) and requires the use of an actual storage to store the cache data - that is, the abstraction frees the developer from having to write the caching logic but does not provide the actual stores.

How do I turn on Spring cache?

We can enable caching in the Spring Boot application by using the annotation @EnableCaching. It is defined in org. springframework. cache.

How does Spring cache work internally?

Spring Cache uses the parameters of the method as key and the return value as a value in the cache. When the method is called the first time, Spring will check if the value with the given key is in the cache. It will not be the case, and the method itself will be executed.


1 Answers

This problem has been addressed in another question and is less about abstract classes and more about the framework's ability to figure out which cache to use.

Long story short (quoting from Spring documentation) you are missing appropriate CacheResolver that will work with your abstract class hierarchy:

Since Spring 4.1, the value attribute of the cache annotations are no longer mandatory, since this particular information can be provided by the CacheResolver regardless of the content of the annotation.

Therefore, your abstract class should define a caching resolver instead of directly stating the cache name.

abstract class Repository<T> {
    // .. some methods omitted for brevity

    @Cacheable(cacheResolver = CachingConfiguration.CACHE_RESOLVER_NAME)
    public List<T> findAll() {
        return getDao().findAll();
    }
}

The resolver determines the Cache instance(s) to use for an intercepted method invocation. A very naive implementation can take the target repository bean (by name) and use it as the cache name

class RuntimeCacheResolver
        extends SimpleCacheResolver {

    protected RuntimeCacheResolver(CacheManager cacheManager) {
        super(cacheManager);
    }

    @Override
    protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
        return Arrays.asList(context.getTarget().getClass().getSimpleName());
    }
}

Such resolver needs an explicit configuration:

@Configuration
@EnableCaching
class CachingConfiguration extends CachingConfigurerSupport {

    final static String CACHE_RESOLVER_NAME = "simpleCacheResolver";

    @Bean
    @Override
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }

    @Bean(CACHE_RESOLVER_NAME)
    public CacheResolver cacheResolver(CacheManager cacheManager) {
        return new RuntimeCacheResolver(cacheManager);
    }
}

I've create a Gist which describes the whole concept in more details.

Disclaimer

The above snippets are just for demonstration and are intended to give direction than to provide a complete solution. The above cache resolver implementation is very naive and doesn't consider many things (like method parameters etc.). I'd never use it in a production environment.

The way Spring handles caching is through proxies, where the @Cacheable annotation declares the cache, together with naming information processed on runtime. The cache is resolved through runtime information provided to cache resolver (no surprise it resembles some similarities to InvocationContext of classical AOP).

public interface CacheOperationInvocationContext<O extends BasicOperation> {
    O getOperation();
    Object getTarget();
    Method getMethod();
    Object[] getArgs();
}

Through the getTarget() method it is possible to figure out which bean is proxied, but in real-life, more information should be taken into account, to provide a reliable cache (like method parameters, etc).

like image 115
Jakub Marchwicki Avatar answered Sep 21 '22 08:09

Jakub Marchwicki