@Cacheable não funciona no Spring Boot? O problema da chamada via this
Num sistema interno que construí para os times comercial e de logística de uma distribuidora aqui de Natal, as telas de relatório estavam lentas mesmo com @Cacheable nos métodos que consultavam o banco de BI. Os logs mostravam a mesma consulta pesada sendo executada a cada clique. A anotação estava lá, o cache estava habilitado com @EnableCaching e mesmo assim nada era cacheado.
A causa era uma das armadilhas mais comuns do Spring: o método anotado era chamado de dentro da própria classe. Este post mostra por que isso acontece e como corrigi.
O código que parecia certo
@Service
public class ComercialService {
public PainelDTO montarPainel(String regiao) {
// chamada interna: passa por "this", não pelo proxy
List<VendaDTO> vendas = this.buscarVendas(regiao);
return new PainelDTO(vendas);
}
@Cacheable("vendas-por-regiao")
public List<VendaDTO> buscarVendas(String regiao) {
return biRepository.consultaPesada(regiao);
}
}
O controller chama montarPainel(), que chama buscarVendas(). Quem lê o código espera que a segunda chamada já venha do cache. Não vem.
Por que o cache é ignorado
O Spring implementa @Cacheable (assim como @Transactional e @Async) com proxies de AOP. O objeto que outros beans recebem por injeção não é a sua classe pura: é um proxy que envolve a classe e verifica o cache antes de repassar a chamada.
- Quando o controller chama um método do serviço, a chamada passa pelo proxy, e a lógica de cache roda.
- Quando o próprio serviço chama
this.buscarVendas(), a chamada vai direto para o objeto real, sem passar pelo proxy. Para o Spring, essa chamada não existiu.
A documentação do Spring chama isso de self-invocation. Não aparece erro nem aviso. O método só executa normalmente, sem cache.
Como confirmar que é isso
- Ligue o log do cache:
logging.level.org.springframework.cache=TRACEnoapplication.properties. Se a chamada interna não gera nenhuma linha de “cache hit” ou “cache miss”, o proxy não está sendo usado. - Coloque um log dentro do método cacheado. Se ele aparece em toda chamada com o mesmo parâmetro, o cache não está funcionando.
- Procure no código chamadas ao método anotado feitas pela mesma classe, com ou sem
this.explícito.
4 formas de corrigir
1. Extrair o método para outro bean (a que eu usei)
É a solução mais simples de entender e a que eu adotei em produção: criei um bean dedicado só para as consultas cacheadas e injetei no serviço original.
@Service
public class ComercialDataCacheService {
@Cacheable("vendas-por-regiao")
public List<VendaDTO> buscarVendas(String regiao) {
return biRepository.consultaPesada(regiao);
}
}
@Service
@RequiredArgsConstructor
public class ComercialService {
private final ComercialDataCacheService cache;
public PainelDTO montarPainel(String regiao) {
return new PainelDTO(cache.buscarVendas(regiao)); // passa pelo proxy
}
}
Além de resolver o cache, a separação deixou claro onde ficam todas as consultas cacheadas, o que facilita invalidar com @CacheEvict depois.
2. Injetar o próprio bean com @Lazy
@Service
public class ComercialService {
@Autowired @Lazy
private ComercialService self;
public PainelDTO montarPainel(String regiao) {
return new PainelDTO(self.buscarVendas(regiao));
}
}
Funciona, mas esconde a dependência circular e confunde quem chega no código depois. Eu evito.
3. AopContext.currentProxy()
Com @EnableAspectJAutoProxy(exposeProxy = true), dá para chamar ((ComercialService) AopContext.currentProxy()).buscarVendas(regiao). Prende o código de negócio ao Spring AOP e é fácil de esquecer. Só vale como remendo temporário.
4. AspectJ em modo de compilação
@EnableCaching(mode = AdviceMode.ASPECTJ) com weaving em tempo de compilação ou de carga resolve a auto-invocação de verdade, porque o código do cache passa a ficar dentro da própria classe. Em troca, o build fica bem mais complexo. Raramente compensa num projeto pequeno ou médio.
Outros motivos para o @Cacheable não funcionar
- Método privado ou
final: o proxy não consegue interceptar. - Falta de
@EnableCachingem alguma classe de configuração. - Objeto criado com
newem vez de injetado pelo Spring: não existe proxy. - Chave instável: parâmetros sem
equals()/hashCode()corretos geram uma chave diferente a cada chamada. - Sem provedor de cache configurado: sem Caffeine, Redis etc., o Spring usa um mapa simples em memória, que some a cada reinício.
A mesma armadilha vale para @Transactional e @Async
Um método @Transactional chamado via this não abre transação. Um @Async chamado via this roda na mesma thread. Se uma dessas anotações “não faz nada”, a primeira coisa a verificar é se a chamada vem de dentro da mesma classe.
Perguntas frequentes
Por que o @Cacheable funciona no controller mas não dentro do serviço?
Porque a chamada que vem do controller passa pelo proxy que o Spring cria em volta do serviço. A chamada interna, via this, vai direto ao objeto real e pula o proxy, então a lógica de cache não roda.
Qual a melhor forma de corrigir a auto-invocação no Spring?
Na maioria dos casos, mover o método anotado para outro bean e injetá-lo. É explícito, fácil de testar e não depende de recursos avançados de AOP.
Isso também afeta @Transactional?
Sim. @Transactional, @Async, @Cacheable e outras anotações baseadas em proxy são ignoradas quando o método é chamado pela própria classe.
Precisa de ajuda com um sistema assim?
A PruPru desenvolve e corrige sistemas web em Java/Spring Boot e React para empresas de Natal/RN e do restante do Brasil. Conte o problema pelo WhatsApp e receba um diagnóstico sem compromisso.
Ver serviços →