Spring Cache @Cacheable - ne fonctionne pas lors de l'appel depuis une autre méthode du même bean

107

Le cache Spring ne fonctionne pas lors de l'appel d'une méthode mise en cache à partir d'une autre méthode du même bean.

Voici un exemple pour expliquer mon problème de manière claire.

Configuration:

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

Service mis en cache:

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

}

Résultat :

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

L' getEmployeeDataappel de méthode utilise le cache employeeDatadans le deuxième appel comme prévu. Mais lorsque la getEmployeeDataméthode est appelée dans la AServiceclasse (in getEmployeeEnrichedData), Cache n'est pas utilisé.

Est-ce ainsi que fonctionne Spring Cache ou est-ce que je manque quelque chose?

Bala
la source
utilisez-vous la même valeur pour someDateparam?
Dewfy
@Dewfy Oui, c'est pareil
Bala

Réponses:

158

Je crois que c'est ainsi que cela fonctionne. D'après ce que je me souviens avoir lu, il existe une classe proxy générée qui intercepte toutes les demandes et répond avec la valeur mise en cache, mais les appels «internes» au sein de la même classe n'obtiendront pas la valeur mise en cache.

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

Seuls les appels de méthode externes arrivant via le proxy sont interceptés. Cela signifie que l'auto-invocation, en fait, une méthode dans l'objet cible appelant une autre méthode de l'objet cible, ne conduira pas à une interception de cache réelle au moment de l'exécution, même si la méthode appelée est marquée @Cacheable.

Shawn D.
la source
1
Eh bien, si vous mettez également le deuxième appel en cache, il ne manquera qu'un seul cache. Autrement dit, seul le premier appel à getEmployeeEnrichedData contournera le cache. Le second appel utilisait le retour précédemment mis en cache du premier appel à getEmployeeEnrichedData.
Shawn D.
1
@Bala J'ai le même problème, ma solution est de passer @Cacheableà DAO: (Si vous avez une meilleure solution, faites-le moi savoir, merci.
VAdaihiep
2
vous pouvez également écrire un service par exemple CacheService et mettre toutes vos méthodes en cache dans le service. Autowire le service dont vous avez besoin et appelez les méthodes. A aidé dans mon cas.
DOUBL3P
Depuis le printemps 4.3, cela pourrait être résolu en utilisant l' @Resourceauto-câblage, voir l'exemple stackoverflow.com/a/48867068/907576
radistao
1
De plus, la @Cacheableméthode externe devrait l'être public, elle ne fonctionne pas sur les méthodes privées de package. Je l'ai trouvé à la dure.
anand le
36

Depuis le printemps 4.3, le problème peut être résolu en utilisant l' auto-autowiring sur l' @Resourceannotation:

@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
la source
2
J'ai essayé cela 4.3.17et cela n'a pas fonctionné, les appels pour selfne pas passer par un proxy et le cache est (toujours) contourné.
Madbreaks
A travaillé pour moi. Cache les hits. J'utilise les dernières dépendances de printemps à cette date.
Tomas Bisciak
suis-je le seul à penser que cela casse les modèles, ressemble à un mélange singleton, etc.
2mia
J'ai utilisé la version de démarrage Spring Boot - 2.1.0.RELEASE, et j'ai eu le même problème. Cette solution particulière a fonctionné comme un charme.
Deepan Prabhu Babu le
18

L'exemple ci-dessous est ce que j'utilise pour frapper le proxy depuis le même bean, il est similaire à la solution de @ mario-eis, mais je le trouve un peu plus lisible (peut-être que ce n'est pas :-). Quoi qu'il en soit, j'aime garder les annotations @Cacheable au niveau du service:

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

Voir aussi Démarrer une nouvelle transaction dans Spring Bean

Molholm
la source
1
L'accès au contexte de l'application, par exemple applicationContext.getBean(SettingService.class);, est l'opposé de l'injection de dépendances. Je suggère d'éviter ce style.
SingleShot
2
Oui, il vaudrait mieux l'éviter, mais je ne vois pas de meilleure solution à ce problème.
molholm
10

Voici ce que je fais pour les petits projets avec une utilisation marginale des appels de méthode dans la même classe. La documentation dans le code est fortement conseillée, car elle peut sembler stridente aux collègues. Mais c'est facile à tester, simple, rapide à réaliser et m'épargne l'instrumentation AspectJ à part entière. Cependant, pour une utilisation plus intensive, je conseillerais la solution 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
la source
1
pouvez-vous donner un exemple avec AspectJ?
Sergio Bilello
Cette réponse est un double de stackoverflow.com/a/34090850/1371329 .
jaco0646
3

Dans mon cas, j'ajoute une variable:

@Autowired
private AService  aService;

J'appelle donc la getEmployeeDataméthode en utilisant leaService

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

}

Il utilisera le cache dans ce cas.

Ibtissam Ibtissama
la source
2

Utilisez le tissage statique pour créer un proxy autour de votre bean. Dans ce cas, même les méthodes `` internes '' fonctionneraient correctement

Dewfy
la source
Qu'est-ce que le "tissage statique"? google n'aide pas beaucoup. Des pointeurs pour comprendre ces concepts?
Bala
@Bala - juste par exemple sur notre projet, nous utilisons un <iajccompilateur (de ant) ​​qui résout tous les aspects de nécessité pour les classes pouvant être mises en cache.
Dewfy
0

J'utilise le bean interne interne ( FactoryInternalCache) avec un vrai cache à cet effet:

@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
la source
0

la solution la plus simple est de loin de faire référence comme ceci:

AService.this.getEmployeeData(date);
Jason
la source