Spring Cache @Cacheable - não funciona ao chamar de outro método do mesmo bean

107

O cache do Spring não está funcionando ao chamar o método em cache de outro método do mesmo bean.

Aqui está um exemplo para explicar meu problema de forma clara.

Configuração:

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

Serviço em 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);
        ...
    }

}

Resultado:

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

A getEmployeeDatachamada do método usa cache employeeDatana segunda chamada conforme o esperado. Mas quando o getEmployeeDatamétodo é chamado dentro da AServiceclasse (in getEmployeeEnrichedData), o Cache não está sendo usado.

É assim que o Spring Cache funciona ou estou faltando alguma coisa?

Bala
fonte
você usa o mesmo valor para someDateparam?
Dewfy
@Dewfy Sim, é o mesmo
Bala

Respostas:

158

Eu acredito que é assim que funciona. Pelo que me lembro de ter lido, há uma classe de proxy gerada que intercepta todas as solicitações e responde com o valor armazenado em cache, mas chamadas 'internas' dentro da mesma classe não obterão o valor armazenado em cache.

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

Somente chamadas de método externo que chegam por meio do proxy são interceptadas. Isso significa que a auto-invocação, na verdade, um método dentro do objeto de destino chamando outro método do objeto de destino, não levará a uma interceptação de cache real em tempo de execução, mesmo se o método invocado estiver marcado com @Cacheable.

Shawn D.
fonte
1
Bem, se você fizer a segunda chamada Cacheable também, ela terá apenas uma falha de cache. Ou seja, apenas a primeira chamada para getEmployeeEnrichedData ignorará o cache. A segunda chamada para ele usaria o retorno previamente armazenado em cache da primeira chamada para getEmployeeEnrichedData.
Shawn D.
1
@Bala Tenho o mesmo problema, minha solução é mudar @Cacheablepara o DAO :( Se você tiver uma solução melhor, por favor, me avise, obrigado.
VAdaihiep
2
você também pode escrever um serviço, por exemplo, CacheService e colocar todos os seus métodos de cache no serviço. Autowire o serviço onde você precisa e chame os métodos. Ajudou no meu caso.
DOUBL3P
Desde o Spring 4.3, isso pode ser resolvido usando @Resourceself-autowiring, veja o exemplo stackoverflow.com/a/48867068/907576
radistao
1
Além disso, o @Cacheablemétodo externo deve ser public, ele não funciona em métodos privados de pacote. Achei da maneira mais difícil.
Anand
36

Desde o Spring 4.3, o problema pode ser resolvido usando self-autowiring sobre @Resourceanotação:

@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);
    }
}
radião
fonte
2
Tentei 4.3.17e não funcionou, chamadas para selfnão passarem por um proxy e o cache (ainda) é ignorado.
Madbreaks de
Funcionou para mim. Ocorrências de cache. Eu uso as dependências do Spring mais recentes a partir desta data.
Tomas Bisciak de
sou o único a pensar que isso quebra padrões, parece uma mistura simples, etc etc?
2mia de
Eu usei a versão inicial do Spring Boot - 2.1.0.RELEASE e tive o mesmo problema. Esta solução em particular funcionou perfeitamente.
Deepan Prabhu Babu
18

O exemplo abaixo é o que eu uso para acessar o proxy de dentro do mesmo bean, é semelhante à solução de @ mario-eis, mas acho que é um pouco mais legível (talvez não seja :-). De qualquer forma, gosto de manter as anotações @Cacheable no nível de serviço:

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

Veja também Iniciando uma nova transação no Spring bean

Molholm
fonte
1
Acessar o contexto do aplicativo, por exemplo applicationContext.getBean(SettingService.class);, é o oposto da injeção de dependência. Eu sugiro evitar esse estilo.
SingleShot
2
Sim, seria melhor evitá-lo, mas não vejo uma solução melhor para este problema.
molholm,
10

Aqui está o que eu faço para pequenos projetos com uso apenas marginal de chamadas de método dentro da mesma classe. A documentação in-code é fortemente recomendada, pois pode parecer difícil para os colegas. Mas é fácil de testar, simples, rápido de conseguir e me poupa da instrumentação AspectJ completa. No entanto, para um uso mais intenso, aconselho a solução 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
fonte
1
você poderia dar um exemplo com AspectJ?
Sergio Bilello
Esta resposta é uma duplicata de stackoverflow.com/a/34090850/1371329 .
jaco0646
3

No meu caso, adiciono a variável:

@Autowired
private AService  aService;

Então, chamo o getEmployeeDatamétodo usando oaService

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

}

Ele usará o cache neste caso.

Ibtissam Ibtissama
fonte
2

Use a tecelagem estática para criar proxy em torno de seu bean. Neste caso, mesmo os métodos "internos" funcionariam corretamente

Orvalhado
fonte
O que é "tecelagem estática"? o google não ajuda muito. Alguma indicação para entender esses conceitos?
Bala
@Bala - apenas por exemplo em nosso projeto, usamos um <iajccompilador (da Ant) que resolve todos os aspectos de necessidade para classes com capacidade de cache.
Dewfy
0

Eu uso o bean interno interno ( FactoryInternalCache) com cache real para esta finalidade:

@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);
    }
}
}
radião
fonte
0

a solução mais fácil, de longe, é apenas fazer referência a esta:

AService.this.getEmployeeData(date);
Jason
fonte