Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring boot caching in @Service class does not work

I have problems with save some values in @Service method. My code:

@Service(value = "SettingsService")
public class SettingsService {
...

    public String getGlobalSettingsValue(Settings setting) {
        getTotalEhCacheSize();
        if(!setting.getGlobal()){
            throw new IllegalStateException(setting.name() + " is not global setting");
        }
        GlobalSettings globalSettings = globalSettingsRepository.findBySetting(setting);
        if(globalSettings != null)
            return globalSettings.getValue();
        else
            return getGlobalEnumValue(setting)
    }

@Cacheable(value = "noTimeCache", key = "#setting.name()")
    public String getGlobalEnumValue(Settings setting) {
        return Settings.valueOf(setting.name()).getDefaultValue();
    }

My repository class:

@Repository
public interface GlobalSettingsRepository extends CrudRepository<GlobalSettings, Settings> {

    @Cacheable(value = "noTimeCache", key = "#setting.name()", unless="#result == null")
    GlobalSettings findBySetting(Settings setting);

It should work like this:

  • get value form DB if data exist,
  • if not save value from enum.

but it didn't save any data from DB or enum.

My cache config:

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public EhCacheCacheManager cacheManager(CacheManager cm) {
        return new EhCacheCacheManager(cm);
    }
    @Bean
    public EhCacheManagerFactoryBean ehcache() {
        EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));

        return  ehCacheManagerFactoryBean;
    }
}

I have some example to make sure that cache is working in my project in rest method:

    @RequestMapping(value = "/system/status", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> systemStatus() {
        Object[] list = userPuzzleRepository.getAverageResponseByDateBetween(startDate, endDate);
...
}

public interface UserPuzzleRepository extends CrudRepository<UserPuzzle, Long> {
    @Cacheable(value = "averageTimeAnswer", key = "#startDate")
    @Query("select AVG(case when up.status='SUCCESS' OR up.status='FAILURE' OR up.status='TO_CHECK' then up.solvedTime else null end) from UserPuzzle up where up.solvedDate BETWEEN ?1 AND ?2")
    Object[] getAverageResponseByDateBetween(Timestamp startDate, Timestamp endDate);

and it's work well.

What am I doing wwrong?

like image 325
CSniper Avatar asked Aug 22 '16 05:08

CSniper


People also ask

How do I enable caching in spring?

To enable the Spring Boot caching feature, you need to add the @EnableCaching annotation to any of your classes annotated with @Configuration or to the boot application class annotated with @SpringBootApplication .

Does spring boot cache by default?

The Spring Boot Framework provides a starter dependency that adds basic cache dependency in the application. The starter cache dependency, by default, provides the spring-context-support dependency.

How does caffeine cache work?

Caffeine provides flexible construction to create a cache with a combination of the following optional features: automatic loading of entries into the cache, optionally asynchronously. size-based eviction when a maximum is exceeded based on frequency and recency.


2 Answers

You have two methods in your SettingsService, one that is cached (getGlobalEnumValue(...)) and another one that isn't cached, but calls the other method (getGlobalSettingsValue(...)).

The way the Spring cache abstraction works however is by proxying your class (using Spring AOP). However, calls to methods within the same class will not call the proxied logic, but the direct business logic beneath. This means caching does not work if you're calling methods in the same bean.

So, if you're calling getGlobalSettingsValue(), it will not populate, nor use the cache when that method calls getGlobalEnumValue(...).


The possible solutions are:

  1. Not calling another method in the same class when using proxies
  2. Caching the other method as well
  3. Using AspectJ rather than Spring AOP, which weaves the code directly into the byte code at compile time, rather than proxying the class. You can switch the mode by setting the @EnableCaching(mode = AdviceMode.ASPECTJ). However, you'll have to set up load time weaving as well.
  4. Autowire the service into your service, and use that service rather than calling the method directly. By autowiring the service, you inject the proxy into your service.
like image 123
g00glen00b Avatar answered Sep 23 '22 18:09

g00glen00b


The problem is in the place you call your cacheable method from. When you call your @Cacheable method from same class, you just call it from this reference, which means it doesn't wrapped by Spring's proxy, so Spring can't catch your invocation to handle it.

One on ways to solve this problem is to @Autowired service to itself and just call methods you expected spring have to handle by this reference:

@Service(value = "SettingsService")
public class SettingsService {
//...

    @Autowired
    private SettingsService settingsService;
//...
    public String getGlobalSettingsValue(Settings setting) {
       // ...
        return settingsSerive.getGlobalEnumValue(setting)
//-----------------------^Look Here
    }

    @Cacheable(value = "noTimeCache", key = "#setting.name()")
    public String getGlobalEnumValue(Settings setting) {
        return Settings.valueOf(setting.name()).getDefaultValue();
    }
}

But if you have such problems it means your classes are take on too much and aren't comply with the principle of "single class - single responsibility". The better solution would be to move method with @Cacheable to dedicated class.

like image 30
Konstantin Labun Avatar answered Sep 22 '22 18:09

Konstantin Labun