Usando Cache com Spring Boot
.webp)

No contexto de software, cache é um componente que mantem em memória um conjunto de dados para que possa, em chamadas futuras, ser acessado mais rapidamente que em outros sistemas de armazenamento, como banco de dados ou arquivos no sistema operacional.
A função de um cache é melhorar a performance do software no que se refere ao acesso a dados. Como normalmente não é possível manter todos os dados usados pelo software (seja em banco de dados ou arquivos) em memória, a ideia é manter em cache os dados mais acessados durante o ciclo de execução do software.
A configuração do cache é muito importante para chegar a um uso eficiente do mesmo. Para isso é importante conhecer os dados que precisam e/ou desejam armazenar em cache, para poder dimensionar tamanho do cache (quantos objetos irá manter), tempo de permanência dos dados no cache(TTL – Time To Live - quanto tempo o objeto será mantido no cache), etc.
Spring Boot e Cache
Esse artigo parte do princípio que o leitor conheça Spring Boot, Spring Data JPA e Banco de Dados.
É muito simples usar a implementação padrão de cache (SimpleCacheManager usando ConcurrentHashMap) numa aplicação Spring Boot. Para isso basta acrescentar a dependência do starter “spring-boot-starter-cache” para que todas as dependências necessárias para habilitar o cache na aplicação sejam adicionadas ao projeto.
Além da implementação padrão, é possível usar também JCache que implementa a especificação JSR-107. Veja referência aqui.
Os provedores de cache suportados pelo Spring Boot são:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-cache'
Configurações
A configuração padrão para o Spring Boot usar o cache é bastante simples, para outras implementações, as configurações variam, algumas delas podem requerer um arquivo XML específico.
Para fazer uso do cache, é necessário aplicar anotações nos métodos que usarão o cache. É possível também fazer isso através de arquivos XML, mas não é o foco desse artigo
As anotações:
@EnableCaching: Essa anotação é que habilita a aplicação a processar as demais anotações de cache.
Ela pode ser aplicada na classe main da aplicação ou na classe de configuração do cache, quando essa classe for necessária.
@SpringBootApplication
@EnableCaching
public class CachingApplication {
@Configuration
@EnableCaching
public class CachingConfig {
@Cacheable: Essa anotação é usada para marcar métodos que o resultado (retorno) será armazenado em cache e, em subsequentes chamadas do método, será retornado o valor que está no cache em vez de executar novamente o método. Essa anotação normalmente é usada nos método que pesquisam objetos.
@Cacheable("books")
public Book getBookByIsbn(String isbn) {
log.info("Get Book by ISBN {}", isbn);
return repository.findByIsbn(isbn).orElse(null);
}
As implementações de cache no fundo é um map (chave, valor) onde a chave no caso são os parâmetros do método, baseado no seguinte algoritmo:
Se não tem parâmetros, retorna 0 (zero)
Apenas um parâmetro, retorna a instância do parâmetro
Mais de um parâmetro, retorna uma chave calculada com os hashes de todos os parâmetros.
Para os casos de mais de um parâmetro, pode-se usar o atributo key para definir a chave para o cache. Pode-se usar SpEL (Spring Expression Language) para pegar o argumento que deseja para gerar a chave.
@Cacheable(value="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
Veja a tabela de metadados SpEL disponíveis para Cache no final do artigo.
@CachePut: Essa anotação é usada para marcar métodos onde são atualizados objetos que estão em cache, substituindo no cache o objeto previamente armazenado antes de sua alteração. Essa anotação suporta as mesmas opções/atributos da @Cacheable.
@CachePut(value = "books", key = "#result.isbn")
public Book updateBook(BookDto dto) {
return repository.save(mapper.toEntity(dto));
}
@CacheEvict: Essa anotação é usada para marcar métodos onde são excluídos/removidos objetos que estão em cache. Essa anotação suporta as mesmas opções/atributos da @Cacheable e tem uma opção a mais, “allEntries” para indicar para remover todas os objetos do cache. Quando essa opção é “true”, não são avaliados os parâmetros do método para identificar o objeto no cache.
@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)
@CacheEvict(value = "books")
public void deleteBookByIsbn(String isbn) {
long deleted = repository.deleteBookByIsbn(isbn);
log.info("Records deleted {}", deleted);
}
Comportamento padrão, mas não intuitivo.
Um método marcado com as anotações de cache não usará o cache se for chamado de dentro da própria classe do método.
Isso ocorre porque o processamento das anotações são realizadas pelas classes proxies criadas automaticamente na inicialização da aplicação. Sendo assim, somente as chamadas que passam por esses proxies farão uso do cache.
Por exemplo, um método que retorne um DTO a partir de uma entidade e esse método usa o método de pesquisa da entidade que tem a anotação @Cacheable, a pesquisa não usará o cache, o método anotado só usará a anotação de cache se chamado de outra classe, diferente da classe que está.
Se precisar usar o método “cacheado” dentro da própria classe, é preciso usar o CacheManager em vez das anotações.
CacheManager:
A classe CacheManager permite total controle do cache, permitindo que sua aplicação pesquise, inclua e remova objetos no cache de acordo com suas necessidades.
Quando se usa a implementação padrão do Spring Boot, ao processar as anotações, em tempo de instanciação da aplicação, é criado um Cache Manager com todos os caches que estão descritos em todas as anotações @Cacheable da aplicação.
Quando do cache manager padrão não é suficiente, é possível criar programaticamente um cache manager dentro de uma classe com a anotação @Configuration e anotar o método com a anotação @Bean.
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("books"),
new ConcurrentMapCache("students")
));
return cacheManager;
}
}
Os principais métodos do CacheManager são:
getCache: pegar o map, quem implementa cache com os objetos armazenados, pelo nome definido.
private Cache getCache() {
return cacheManager.getCache("students");
}
put: inserir um objeto no map do cache definido, deve passar a chave e objeto a ser inserido no cache.
public Student saveStudent(StudentDto dto) {
Student student = repository.save(mapper.toEntity(dto));
getCache().put(dto.getEnrollment(), student);
return student;
}
get: pegar um objeto do map do cache pela sua chave.
public Student getStudentByEnrollment(Integer enrollment) {
Student student = getCache().get(enrollment, Student.class);
if (student != null) {
return student;
}
student = repository.findByEnrollment(enrollment).orElse(null);
getCache().put(enrollment, student);
return student;
}
evict: remove um objeto do map do cache pela sua chave.
public void deleteStudent(StudentDto dto) {
repository.delete(mapper.toEntity(dto));
getCache().evict(dto.getEnrollment());
}
Cuidados:
Em uma aplicação multi-thread, o acesso ao cache deve ser “thread safe”. Para isso há um atributo nas anotações, chamado “sync=<boolean>” que quando é true torna o cache sincronizado.
Para aplicações que estão em nuvem e que podem ter várias réplicas para prover escalabilidade, quando o cache está interno à aplicação, cada instância terá seu próprio cache, ou seja poderá haver duplicidade de dados nos caches, bem como uma instância precisar ia buscar o dado no banco quando esse já está no cache de outra instância. Para evitar esse cenário, o cache não deve estar embutido na aplicação, mas fora dela, provido por implementação que suporte essa característica, como o Redis.
Claro que isso depende de estudos mais profundos das necessidades da aplicação e avaliação do custo/benefício da solução. Pois pode ser que o custo dessa solução, ter um cache externo, acabe sendo superior aos “problemas” apontados no parágrafo acima. Cada caso é um caso.
Tabela de metadados SpEL disponíveis para Cache

Tabela de metadados SpEL disponíveis para Cache

O código completo está disponível em https://github.com/joaocesar-dev/SpringBootWithCache.
Biografia
João Cesar Pereira
Engenheiro de Software Sr
Trabalhando com Java desde 2011 e apaixonado por desenvolvimento de software desde ... muito tempo :)
Referências:
https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache
https://spring.io/guides/gs/caching/
