Spring Cache @Cacheable - funktioniert nicht, wenn von einer anderen Methode derselben Bean aufgerufen wird

107

Der Spring-Cache funktioniert nicht, wenn die zwischengespeicherte Methode von einer anderen Methode derselben Bean aufgerufen wird.

Hier ist ein Beispiel, um mein Problem klar zu erklären.

Aufbau:

<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"/>  

Zwischengespeicherter Dienst:

@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);
        ...
    }

}

Ergebnis:

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

Der getEmployeeDataMethodenaufruf verwendet employeeDataerwartungsgemäß den Cache im zweiten Aufruf. Wenn die getEmployeeDataMethode jedoch innerhalb der AServiceKlasse (in getEmployeeEnrichedData) aufgerufen wird , wird der Cache nicht verwendet.

Funktioniert der Spring Cache so oder fehlt mir etwas?

Bala
quelle
Verwenden Sie den gleichen Wert für someDateparam?
Dewfy
@ Dewfy Ja, es ist das gleiche
Bala

Antworten:

158

Ich glaube, so funktioniert es. Soweit ich mich erinnere, wurde eine Proxy-Klasse generiert, die alle Anforderungen abfängt und mit dem zwischengespeicherten Wert antwortet, aber 'interne' Aufrufe innerhalb derselben Klasse erhalten den zwischengespeicherten Wert nicht.

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

Es werden nur externe Methodenaufrufe abgefangen, die über den Proxy eingehen. Dies bedeutet, dass der Selbstaufruf einer Methode innerhalb des Zielobjekts, die eine andere Methode des Zielobjekts aufruft, zur Laufzeit nicht zu einem tatsächlichen Abfangen des Caches führt, selbst wenn die aufgerufene Methode mit @Cacheable markiert ist.

Shawn D.
quelle
1
Wenn Sie den zweiten Aufruf auch zwischenspeicherbar machen, wird nur ein Cache-Fehler auftreten. Das heißt, nur der erste Aufruf von getEmployeeEnrichedData umgeht den Cache. Der zweite Aufruf würde die zuvor zwischengespeicherte Rückgabe vom ersten Aufruf an getEmployeeEnrichedData verwenden.
Shawn D.
1
@ Bala Ich habe das gleiche Problem, meine Lösung ist die Umstellung @Cacheableauf DAO :( Wenn Sie eine bessere Lösung haben, lassen Sie es mich bitte wissen, danke.
VAdaihiep
2
Sie können auch einen Dienst schreiben, z. B. CacheService, und alle Ihre Cache-Methoden in den Dienst einfügen. Autowire den Service, wo Sie benötigen, und rufen Sie die Methoden auf. Hat in meinem Fall geholfen.
DOUBL3P
Seit Spring 4.3 konnte dies mit @ResourceSelf-Autowiring gelöst werden , siehe Beispiel stackoverflow.com/a/48867068/907576
radistao
1
Auch die externe @CacheableMethode sollte sein public, sie funktioniert nicht bei paketprivaten Methoden. Fand es auf die harte Tour.
4.
36

Seit Spring 4.3 konnte das Problem durch Selbstautowire über @ResourceAnnotation gelöst werden :

@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
quelle
2
Versucht dies unter 4.3.17und es hat nicht funktioniert, Anrufe, selfum nicht durch einen Proxy zu gehen und der Cache wird (noch) umgangen.
Madbreaks
Hat für mich gearbeitet. Cache-Treffer. Ab diesem Datum verwende ich die neuesten Frühlingsabhängigkeiten.
Tomas Bisciak
Bin ich der einzige, der denkt, dass dies Muster bricht, wie eine Singleton-Mischung aussieht usw. usw.?
2mia
Ich habe Spring Boot Starter Version - 2.1.0.RELEASE verwendet, und ich hatte das gleiche Problem. Diese besondere Lösung wirkte wie ein Zauber.
Deepan Prabhu Babu
18

Das folgende Beispiel ist das, was ich verwende, um den Proxy aus derselben Bean heraus zu treffen. Es ähnelt der Lösung von @ mario-eis, finde es aber etwas lesbarer (vielleicht nicht :-). Wie auch immer, ich möchte die @ Cache-Anmerkungen auf dem Service-Level halten:

@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);
}
...

Siehe auch Neue Transaktion in Spring Bean starten

Molholm
quelle
1
Der Zugriff auf den Anwendungskontext applicationContext.getBean(SettingService.class);ist beispielsweise das Gegenteil von Abhängigkeitsinjektion. Ich schlage vor, diesen Stil zu vermeiden.
SingleShot
2
Ja, es wäre besser, dies zu vermeiden, aber ich sehe keine bessere Lösung für dieses Problem.
Molholm
10

Folgendes mache ich für kleine Projekte mit nur geringfügiger Verwendung von Methodenaufrufen innerhalb derselben Klasse. In-Code-Dokumentation wird dringend empfohlen, da sie für Kollegen möglicherweise unangemessen ist. Aber es ist einfach zu testen, einfach, schnell zu erreichen und erspart mir die volle AspectJ-Instrumentierung. Für eine stärkere Nutzung würde ich jedoch die AspectJ-Lösung empfehlen.

@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
quelle
1
Können Sie ein Beispiel mit AspectJ geben?
Sergio Bilello
Diese Antwort ist ein Duplikat von stackoverflow.com/a/34090850/1371329 .
jaco0646
3

In meinem Fall füge ich Variable hinzu:

@Autowired
private AService  aService;

Also rufe ich die getEmployeeDataMethode mit dem aufaService

@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);
    ...
}

}}

In diesem Fall wird der Cache verwendet.

Ibtissam Ibtissama
quelle
2

Verwenden Sie statisches Weben, um einen Proxy um Ihre Bohne zu erstellen. In diesem Fall würden sogar 'interne' Methoden korrekt funktionieren

Dewfy
quelle
Was ist "statisches Weben"? Google hilft nicht viel. Irgendwelche Hinweise, um diese Konzepte zu verstehen?
Bala
@Bala - nur zum Beispiel verwenden wir in unserem Projekt einen <iajcCompiler (von ant), der alle notwendigen Aspekte für cachefähige Klassen auflöst.
Dewfy
0

Ich benutze interne innere Bean ( FactoryInternalCache) mit echtem Cache für diesen Zweck:

@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
quelle
0

Die mit Abstand einfachste Lösung besteht darin, einfach so zu referenzieren:

AService.this.getEmployeeData(date);
Jason
quelle