Spring Cache @Cacheable - nie działa podczas wywoływania z innej metody tego samego beana

108

Spring cache nie działa podczas wywoływania metody buforowanej z innej metody tego samego beana.

Oto przykład, który wyjaśnia mój problem w jasny sposób.

Konfiguracja:

<cache:annotation-driven cache-manager="myCacheManager" />

<bean id="myCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="myCache" />
</bean>

<!-- Ehcache library setup -->
<bean id="myCache"
    class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:shared="true">
    <property name="configLocation" value="classpath:ehcache.xml"></property>
</bean>

<cache name="employeeData" maxElementsInMemory="100"/>  

Usługa buforowana:

@Named("aService")
public class AService {

    @Cacheable("employeeData")
    public List<EmployeeData> getEmployeeData(Date date){
    ..println("Cache is not being used");
    ...
    }

    public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
        List<EmployeeData> employeeData = getEmployeeData(date);
        ...
    }

}

Wynik:

aService.getEmployeeData(someDate);
output: Cache is not being used
aService.getEmployeeData(someDate); 
output: 
aService.getEmployeeEnrichedData(someDate); 
output: Cache is not being used

Do getEmployeeDatazastosowania metody połączeń buforować employeeDataw drugim naborze, jak oczekiwano. Ale gdy getEmployeeDatametoda jest wywoływana w AServiceklasie (in getEmployeeEnrichedData), pamięć podręczna nie jest używana.

Czy tak działa Spring Cache, czy czegoś mi brakuje?

Bala
źródło
czy używasz tej samej wartości dla someDateparametru?
Dewfy
@Dewfy Tak, to samo
Bala

Odpowiedzi:

162

Wierzę, że tak to działa. Z tego, co pamiętam, czytałem, że jest wygenerowana klasa proxy, która przechwytuje wszystkie żądania i odpowiada za pomocą wartości z pamięci podręcznej, ale wywołania „wewnętrzne” w ramach tej samej klasy nie otrzymają wartości z pamięci podręcznej.

Od https://code.google.com/p/ehcache-spring-annotations/wiki/UsingCacheable

Przechwytywane są tylko wywołania metod zewnętrznych przychodzące przez proxy. Oznacza to, że samo wywołanie, w efekcie metoda w obiekcie docelowym wywołująca inną metodę obiektu docelowego, nie doprowadzi do faktycznego przechwycenia pamięci podręcznej w czasie wykonywania, nawet jeśli wywoływana metoda jest oznaczona @Cacheable.

Shawn D.
źródło
1
Cóż, jeśli drugie połączenie również ustawisz jako buforowane, będzie mieć tylko jeden brak pamięci podręcznej. Oznacza to, że tylko pierwsze wywołanie metody getEmployeeEnrichedData pominie pamięć podręczną. Drugie wywołanie tego polecenia użyłoby wcześniej zapisanego w pamięci podręcznej powrotu z pierwszego wywołania metody getEmployeeEnrichedData.
Shawn D.
1
@Bala Mam ten sam problem, moim rozwiązaniem jest przejście @Cacheablena DAO :( Jeśli masz lepsze rozwiązanie, daj mi znać, dzięki.
VAdaihiep
2
możesz także napisać usługę, np. CacheService i umieścić wszystkie swoje metody do pamięci podręcznej w usłudze. Autopowirej usługę tam, gdzie potrzebujesz i wywołaj metody. Pomogło w moim przypadku.
DOUBL3P
1
Od wiosny 4.3 można to rozwiązać za pomocą @Resourcesamoczynnego okablowania, patrz przykład stackoverflow.com/a/48867068/907576
radistao
1
@CacheablePowinna być również metoda zewnętrzna public, która nie działa z metodami prywatnymi pakietu. Znalazłem to na własnej skórze.
an i
38

Od wiosny 4.3 problem można rozwiązać za pomocą samoczynnego okablowania nad @Resourceadnotacją:

@Component
@CacheConfig(cacheNames = "SphereClientFactoryCache")
public class CacheableSphereClientFactoryImpl implements SphereClientFactory {

    /**
     * 1. Self-autowired reference to proxified bean of this class.
     */
    @Resource
    private SphereClientFactory self;

    @Override
    @Cacheable(sync = true)
    public SphereClient createSphereClient(@Nonnull TenantConfig tenantConfig) {
        // 2. call cached method using self-bean
        return self.createSphereClient(tenantConfig.getSphereClientConfig());
    }

    @Override
    @Cacheable(sync = true)
    public SphereClient createSphereClient(@Nonnull SphereClientConfig clientConfig) {
        return CtpClientConfigurationUtils.createSphereClient(clientConfig);
    }
}
radistao
źródło
2
Próbowałem tego 4.3.17i to nie zadziałało, wywołania selfnie przechodzą przez proxy, a pamięć podręczna jest (nadal) pomijana.
Madbreaks
Pracował dla mnie. Trafienia w pamięci podręcznej. Używam najnowszych zależności wiosennych od tej daty.
Tomas Bisciak
Czy tylko ja uważam, że to łamie schematy, wygląda jak singleton mix itp.?
2mia
Użyłem wersji Spring Boot Starter - 2.1.0.RELEASE i miałem ten sam problem. To szczególne rozwiązanie działało jak urok.
Deepan Prabhu Babu
Czy to nie spowoduje cyklicznej zależności?
Chandresh Mishra
18

Poniższy przykład jest tym, czego używam, aby trafić do proxy z tego samego ziarna, jest podobny do rozwiązania @ mario-eis, ale uważam, że jest nieco bardziej czytelny (może nie jest :-). W każdym razie lubię utrzymywać adnotacje @Cacheable na poziomie usług:

@Service
@Transactional(readOnly=true)
public class SettingServiceImpl implements SettingService {

@Inject
private SettingRepository settingRepository;

@Inject
private ApplicationContext applicationContext;

@Override
@Cacheable("settingsCache")
public String findValue(String name) {
    Setting setting = settingRepository.findOne(name);
    if(setting == null){
        return null;
    }
    return setting.getValue();
}

@Override
public Boolean findBoolean(String name) {
    String value = getSpringProxy().findValue(name);
    if (value == null) {
        return null;
    }
    return Boolean.valueOf(value);
}

/**
 * Use proxy to hit cache 
 */
private SettingService getSpringProxy() {
    return applicationContext.getBean(SettingService.class);
}
...

Zobacz także Rozpoczynanie nowej transakcji w ziarnie wiosennym

molholm
źródło
1
Dostęp do kontekstu aplikacji applicationContext.getBean(SettingService.class);jest na przykład przeciwieństwem wstrzykiwania zależności. Proponuję unikać tego stylu.
SingleShot
2
Tak, lepiej byłoby tego uniknąć, ale nie widzę lepszego rozwiązania tego problemu.
molholm
10

Oto, co robię dla małych projektów z tylko marginalnym wykorzystaniem wywołań metod w tej samej klasie. Dokumentacja w kodzie jest zdecydowanie zalecana, ponieważ może wydawać się przerażająca dla kolegów. Ale jest łatwy do przetestowania, prosty, szybki do osiągnięcia i oszczędza mi pełnowartościowe oprzyrządowanie AspectJ. Jednak w przypadku bardziej intensywnego użytkowania radziłbym rozwiązanie AspectJ.

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class AService {

    private final AService _aService;

    @Autowired
    public AService(AService aService) {
        _aService = aService;
    }

    @Cacheable("employeeData")
    public List<EmployeeData> getEmployeeData(Date date){
        ..println("Cache is not being used");
        ...
    }

    public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
        List<EmployeeData> employeeData = _aService.getEmployeeData(date);
        ...
    }
}
Mario Eis
źródło
1
czy mógłbyś podać przykład z AspectJ?
Sergio Bilello
Ta odpowiedź jest duplikatem strony stackoverflow.com/a/34090850/1371329 .
jaco0646
3

W moim przypadku dodaję zmienną:

@Autowired
private AService  aService;

Dlatego wywołuję tę getEmployeeDatametodę przy użyciuaService

@Named("aService")
public class AService {

@Cacheable("employeeData")
public List<EmployeeData> getEmployeeData(Date date){
..println("Cache is not being used");
...
}

public List<EmployeeEnrichedData> getEmployeeEnrichedData(Date date){
    List<EmployeeData> employeeData = aService.getEmployeeData(date);
    ...
}

}

W tym przypadku użyje pamięci podręcznej.

Ibtissam Ibtissama
źródło
2

Użyj statycznego tkania, aby utworzyć proxy wokół fasoli. W takim przypadku nawet metody „wewnętrzne” działałyby poprawnie

Dewfy
źródło
Co to jest „tkanie statyczne”? Google niewiele pomaga. Jakieś wskazówki, aby zrozumieć te pojęcia?
Bala
@Bala - na przykład w naszym projekcie używamy <iajckompilatora (od ant), który rozwiązuje wszystkie aspekty konieczności dla klas buforowalnych.
Dewfy
0

W tym celu używam wewnętrznej wewnętrznej fasoli ( FactoryInternalCache) z prawdziwą pamięcią podręczną:

@Component
public class CacheableClientFactoryImpl implements ClientFactory {

private final FactoryInternalCache factoryInternalCache;

@Autowired
public CacheableClientFactoryImpl(@Nonnull FactoryInternalCache factoryInternalCache) {
    this.factoryInternalCache = factoryInternalCache;
}

/**
 * Returns cached client instance from cache.
 */
@Override
public Client createClient(@Nonnull AggregatedConfig aggregateConfig) {
    return factoryInternalCache.createClient(aggregateConfig.getClientConfig());
}

/**
 * Returns cached client instance from cache.
 */
@Override
public Client createClient(@Nonnull ClientConfig clientConfig) {
    return factoryInternalCache.createClient(clientConfig);
}

/**
 * Spring caching feature works over AOP proxies, thus internal calls to cached methods don't work. That's why
 * this internal bean is created: it "proxifies" overloaded {@code #createClient(...)} methods
 * to real AOP proxified cacheable bean method {@link #createClient}.
 *
 * @see <a href="/programming/16899604/spring-cache-cacheable-not-working-while-calling-from-another-method-of-the-s">Spring Cache @Cacheable - not working while calling from another method of the same bean</a>
 * @see <a href="/programming/12115996/spring-cache-cacheable-method-ignored-when-called-from-within-the-same-class">Spring cache @Cacheable method ignored when called from within the same class</a>
 */
@EnableCaching
@CacheConfig(cacheNames = "ClientFactoryCache")
static class FactoryInternalCache {

    @Cacheable(sync = true)
    public Client createClient(@Nonnull ClientConfig clientConfig) {
        return ClientCreationUtils.createClient(clientConfig);
    }
}
}
radistao
źródło
0

zdecydowanie najłatwiejszym rozwiązaniem jest odwołanie się w ten sposób:

AService.this.getEmployeeData(date);
Jason
źródło