I Solução de cache baseada no SpringCache
1.1 Introdução ao SpringCache
springcache foi lançado na versão spring3.1. Ele encapsula e abstrai o uso de cache, permitindo obter resultados em cache simplesmente usando anotações nos métodos. Justamente por usar anotações, resolve o problema de acoplamento entre o código de negócio e o código de cache, permitindo que o código existente passe a oferecer suporte a cache imediatamente, sem interferir no código de negócio, de forma transparente para os desenvolvedores.
1.2 Visão geral do SpringCache
Existem muitos frameworks de cache, cada um com suas vantagens, como Redis, Memcached, Guava, Caffeine, entre outros.
Se nosso programa quiser usar cache, precisará se acoplar a esses frameworks. Arquitetos atentos já usam interfaces para reduzir esse acoplamento, aproveitando a abstração e o polimorfismo da orientação a objetos para separar o código de negócio dos frameworks específicos.
Mas ainda precisamos chamar explicitamente no código as interfaces e os métodos relacionados ao cache, inserindo dados no cache e lendo-os dele nos momentos adequados. Pense nos cenários de uso de 「AOP」,não é exatamente algo que deveria naturalmente ser feito com AOP?
Desde o Spring
3.1, há suporte a cache por anotações, semelhante ao gerenciamento de transações com a anotação [@Transactional](/Transactional ), além de uma abstração de cache; antes disso, a implementação geralmente era feita com AOP.
Vantagens de usar o Spring Cache:
-
Fornece uma abstração básica de cache, facilitando a troca entre diferentes implementações subjacentes de cache;
-
Com a anotação Cache, é possível aplicar a lógica de cache de forma transparente ao nosso código de negócio, de maneira semelhante às transações, e fazer isso com menos código;
-
Reverter automaticamente o cache também quando houver rollback da transação;
-
Suporta lógicas de cache relativamente complexas;

1.3 Projeto central do Spring Cache
A partir do Spring 3.1, foi introduzido o Spring Cache, a abstração de cache do Spring. Ao definir org.springframework.cache.Cache e org.springframework.cache.CacheManager interface para unificar diferentes tecnologias de cache e oferecer suporte ao uso de anotações JCache para simplificar o desenvolvimento.
A interface Cache define o padrão dos componentes de cache e reúne suas diversas operações. O Spring fornece várias implementações da interface Cache xxxCache implementação de: RedisCache , EhCacheCache , ConcurrentMapCache etc.
Vamos conhecer a interface Cache e, por meio de alguns trechos do código-fonte, CacheManager interface.
Interface Cache
public interface Cache { //Cache名称 String getName(); //Cache负责缓存的对象 Object getNativeCache(); /** * 获取key对应的ValueWrapper * 没有对应的key,则返回null * key对应的value是null,则返回null对应的ValueWrapper */ @Nullable Cache.ValueWrapper get(Object key); //返回key对应type类型的value @Nullable <T> T get(Object key, @Nullable Class<T> type); //返回key对应的value,没有则缓存Callable::call,并返回 @Nullable <T> T get(Object key, Callable<T> valueLoader); //缓存目标key-value(替换旧值),不保证实时性 void put(Object key, @Nullable Object value); //插入缓存,并返回该key对应的value;先调用get,不存在则用put实现 @Nullable default Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Cache.ValueWrapper existingValue = this.get(key); if (existingValue == null) { this.put(key, value); } return existingValue; } //删除缓存,不保证实时性 void evict(Object key); //立即删除缓存:返回false表示剔除前不存在制定key活不确定是否存在;返回true,表示该key之前存在 default boolean evictIfPresent(Object key) { this.evict(key); return false; } //清除所有缓存,不保证实时性 void clear(); //立即清楚所有缓存,返回false表示清除前没有缓存或不能确定是否有;返回true表示清除前有缓存 default boolean invalidate() { this.clear(); return false; } public static class ValueRetrievalException extends RuntimeException { @Nullable private final Object key; public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) { super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); this.key = key; } @Nullable public Object getKey() { return this.key; } } //缓存值的一个包装器接口,实现类为SimpleValueWrapper @FunctionalInterface public interface ValueWrapper { @Nullable Object get(); } }
Como se pode observar, a interface Cache abstrai operações relacionadas ao cache, como get, put e evict.
Classe abstrata AbstractValueAdaptingCache
public abstract class AbstractValueAdaptingCache implements Cache { //是否允许Null值 private final boolean allowNullValues; protected AbstractValueAdaptingCache(boolean allowNullValues) { this.allowNullValues = allowNullValues; } public final boolean isAllowNullValues() { return this.allowNullValues; } @Nullable public ValueWrapper get(Object key) { return this.toValueWrapper(this.lookup(key)); } @Nullable public <T> T get(Object key, @Nullable Class<T> type) { //查询到的缓存值做fromStoreValue转换 Object value = this.fromStoreValue(this.lookup(key)); //转换后非null值且无法转换为type类型则抛出异常 if (value != null && type != null && !type.isInstance(value)) { throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value); } else { return value; } } //从缓存中获取key对应的value,子类实现 @Nullable protected abstract Object lookup(Object key); //对于从缓存中获取的值,允许为空且值为NullValue时,处理为null @Nullable protected Object fromStoreValue(@Nullable Object storeValue) { return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue; } //对于要插入缓存的null值,在允许null值的情况下处理为NullValue;否则抛出异常IllegalArgumentException protected Object toStoreValue(@Nullable Object userValue) { if (userValue == null) { if (this.allowNullValues) { return NullValue.INSTANCE; } else { throw new IllegalArgumentException("Cache '" + this.getName() + "' is configured to not allow null values but null was provided"); } } else { return userValue; } } //get操作依据,查询到缓存值非null,则fromStoreValue转换后包装成SimpleValueWrapper返回 @Nullable protected ValueWrapper toValueWrapper(@Nullable Object storeValue) { return storeValue != null ? new SimpleValueWrapper(this.fromStoreValue(storeValue)) : null; } }
Classe abstrata AbstractValueAdaptingCache implementa a interface Cache e abstrai principalmente a lógica de tratamento de valores NULL.
-
allowNullValuesO atributo indica se é permitido armazenar valores NULL em cache -
fromStoreValuedo método que trata valores NULLgetoperação, na propriedadeallowNullValuesquando for true, tratar NullValue como NULL -
toStoreValuemétodo para tratar valores NULLputoperação, na propriedadeallowNullValuesquando for true, tratar NULL como NullValue; caso contrário, lançar uma exceção -
toValueWrappero método forneceCachena interfacegetimplementação padrão do método, lê o valor do cache e depois, por meio defromStoreValueconversão, e por fim encapsulado comoSimpleValueWrapperVoltar -
ValueWrapper get(Object key)eT get(Object key, @Nullable Classtype)o método é implementado com base no método acima -
ValueWrapper get(Object key)e@Nullable Classtype)o método é implementado com base no método acima -
lookupO método abstrato permite que as subclasses obtenham o valor real do cache
ConcurrentMapCache
public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final String name; //定义ConcurrentMap缓存 private final ConcurrentMap<Object, Object> store; //如果要缓存的是值对象的copy,则由此序列化代理类处理 @Nullable private final SerializationDelegate serialization; public ConcurrentMapCache(String name) { this(name, new ConcurrentHashMap(256), true); } //默认允许处理null public ConcurrentMapCache(String name, boolean allowNullValues) { this(name, new ConcurrentHashMap(256), allowNullValues); } //默认serialization = null public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) { this(name, store, allowNullValues, (SerializationDelegate)null); } protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) { super(allowNullValues); Assert.notNull(name, "Name must not be null"); Assert.notNull(store, "Store must not be null"); this.name = name; this.store = store; this.serialization = serialization; } //serialization不为空,缓存值对象的copy public final boolean isStoreByValue() { return this.serialization != null; } public final String getName() { return this.name; } public final ConcurrentMap<Object, Object> getNativeCache() { return this.store; } //实现lookup:store#get @Nullable protected Object lookup(Object key) { return this.store.get(key); } //基于ConcurrentMap::computeIfAbsent方法实现;get和put的值由fromStoreValue和toStoreValue处理Null @Nullable public <T> T get(Object key, Callable<T> valueLoader) { return this.fromStoreValue(this.store.computeIfAbsent(key, (k) -> { try { return this.toStoreValue(valueLoader.call()); } catch (Throwable var5) { throw new ValueRetrievalException(key, valueLoader, var5); } })); } public void put(Object key, @Nullable Object value) { this.store.put(key, this.toStoreValue(value)); } @Nullable public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Object existing = this.store.putIfAbsent(key, this.toStoreValue(value)); return this.toValueWrapper(existing); } public void evict(Object key) { this.store.remove(key); } public boolean evictIfPresent(Object key) { return this.store.remove(key) != null; } public void clear() { this.store.clear(); } public boolean invalidate() { boolean notEmpty = !this.store.isEmpty(); this.store.clear(); return notEmpty; } protected Object toStoreValue(@Nullable Object userValue) { Object storeValue = super.toStoreValue(userValue); if (this.serialization != null) { try { return this.serialization.serializeToByteArray(storeValue); } catch (Throwable var4) { throw new IllegalArgumentException("Failed to serialize cache value '" + userValue + "'. Does it implement Serializable?", var4); } } else { return storeValue; } } protected Object fromStoreValue(@Nullable Object storeValue) { if (storeValue != null && this.serialization != null) { try { return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[])((byte[])storeValue))); } catch (Throwable var3) { throw new IllegalArgumentException("Failed to deserialize cache value '" + storeValue + "'", var3); } } else { return super.fromStoreValue(storeValue); } } }
ConcurrentMapCache herda a classe abstrata AbstractValueAdaptingCache , é a implementação padrão de cache do Spring. Ela oferece cache por cópia dos objetos armazenados, com a serialização tratada por SerializationDelegate
serialization, cujo padrão é null, ou seja, cache por referência. As operações relacionadas ao cache se baseiam na classe-base AbstractValueAdaptingCache no tratamento de valores
null, permitindo null por padrão.
CacheManager
public interface CacheManager { @Nullable //获取指定name的Cache,可能延迟创建 Cache getCache(String name); //获取当前CacheManager下的Cache name集合 Collection<String> getCacheNames(); }
CacheManager Gerencia um conjunto de Cache com base em
name. Naturalmente, CacheManager também possui muitas classes de implementação, como ConcurrentMapCacheManager 、 AbstractCacheManager e SimpleCacheManager ,esses xxxCacheManager as classes servem para estabelecer as regras de gerenciamento do Cache, por isso não vamos nos aprofundar mais aqui.
II Uso do SpringCache
2.1 Como usar
O Spring Cache é um framework desse tipo. Ele usa AOP para implementar cache baseado em anotações e oferece uma abstração adequada. O código de negócio não precisa se preocupar com qual framework de cache é usado nas camadas inferiores;basta adicionar uma anotação para ter a funcionalidade de cache.
Adicionar dependências
<!--springboot的cache支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--配置拓展还需添加的依赖--> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.9.2</version> </dependency>
Configurar o Cache
Adicione a anotação @EnableCaching à classe de inicialização Application
@SpringBootApplication @MapperScan("com.xxxx.dao") @EnableCaching public class AxxxxApplication { public static void main(String[] args) { SpringApplication.run(AxxxxApplication.class, args); } }
Configurar o tipo de cache; aqui, o foco é o cache local
//方式一:ConcurrentMapCache (不好设置过期时间) @Configuration public class SpringCachingConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); Set<ConcurrentMapCache> mapCaches=new HashSet<>(); //配置cache类型与目录 mapCaches.add(new ConcurrentMapCache("cacheNames1")); mapCaches.add(new ConcurrentMapCache("cacheNames2")); mapCaches.add(new ConcurrentMapCache("cacheNames3")); mapCaches.add(new ConcurrentMapCache("cacheNames4")); cacheManager.setCaches(mapCaches); cacheManager.afterPropertiesSet(); return cacheManager; } } //方式二:CaffeineCache (可设置过期时间) @Configuration public class SpringCachingConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); List<CaffeineCache> caffeineCaches = new ArrayList<>(); //SysConstants.CACHE_NAMES=new ArrayList( //Arrays.asList("cacheNames1","cacheNames2","cacheNames3","cacheNames4") //); for (String cacheName: SysConstants.CACHE_NAMES) { caffeineCaches.add(new CaffeineCache(cacheName, Caffeine.newBuilder() .initialCapacity(50) .maximumSize(30000)//最大存储数量 .expireAfterWrite(2, TimeUnit.HOURS)//过期时间2 小时 .build())); } cacheManager.setCaches(caffeineCaches); return cacheManager; } } //方式三。。。。还可以配置redis以及其他的方式,这里主要介绍以上两种方式
Implementar cache de interfaces
//控制器1 @RestController @RequestMapping("/path1") @Cacheable(cacheNames = "cacheNames1" ,key="#root.methodName+#a0" ) public class Controller1 { } //控制器2 @RestController @RequestMapping("/path2") @Cacheable(cacheNames = "cacheNames2" ,key="#root.methodName+#a0" ) public class Controller2 { } //控制器3 @RestController @RequestMapping("/path3") @Cacheable(cacheNames = "cacheNames3" ,key="#root.methodName+#a0" ) public class Controller3 { } //控制器4 @RestController @RequestMapping("/path4") @Cacheable(cacheNames = "cacheNames4" ,key="#root.methodName+#a0" ) public class Controller4 { }
2.2 Explicação detalhada das anotações SpringCache
Anotação @Cacheable
Se não estiver no cache: consultar o banco de dados, armazenar no cache, retornar o resultado,
Se estiver no cache: retornar o resultado diretamente
| Parâmetros | Explicação | exemplo |
| value | Nome do cache, definido no arquivo de configuração do spring ; é obrigatório especificar pelo menos um |
Por exemplo: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
| key | A chave do cache pode ficar vazia; se for especificada, deverá ser escrita como uma expressão SpEL; se não for especificada, por padrão será formada pela combinação de todos os parâmetros do método | @Cacheable(value=”testcache”,key=”#userName”) |
| condição | Condição de armazenamento em cache, pode ficar vazia, é escrita em SpEL e retorna true ou false; o armazenamento em cache só ocorre quando o resultado é true | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
Função:Pode ser usada para gravar no cache, armazenando o resultado para que, nas chamadas seguintes, o valor em cache possa ser retornado diretamente, sem executar novamente o método real. A forma mais simples de uso,nome da anotação=nome do cache,exemplo de uso abaixo:
@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 System.out.println("real query account."+userName); return getFromDB(userName); }
Anotação @CacheEvict
@CacheEvict: anotação para excluir o cache, muito útil para remover dados antigos e desnecessários. Há ainda um parâmetro adicional aqui (allEntries); ao definir allEntries=true, é possível excluir todas as entradas em lote
| Parâmetros | Explicação | exemplo |
| value | Nome do cache, definido no arquivo de configuração do spring ; é obrigatório especificar pelo menos um | @CacheEvict(value=”my cache”) |
| key | A chave do cache pode ficar vazia; se for especificada, deverá ser escrita como uma expressão SpEL; se não for especificada, por padrão será formada pela combinação de todos os parâmetros do método | @CacheEvict(value=”testcache”,key=”#userName”) |
| condição | Condição de armazenamento em cache, pode ficar vazia, é escrita em SpEL e retorna true ou false; o armazenamento em cache só ocorre quando o resultado é true | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
| allEntries | Define se todo o conteúdo do cache deve ser limpo; o padrão é false. Se for definido como true, todo o cache será limpo imediatamente após a chamada do método | @CachEvict(value=”testcache”,allEntries=true) |
| beforeInvocation | Define se o cache deve ser limpo antes da execução do método; o padrão é false. Se for definido como true, o cache será limpo antes de o método ser executado. Por padrão, se a execução do método lançar uma exceção, o cache não será limpo | @CachEvict(value=”testcache”,beforeInvocation=true) |
@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存 public void updateAccount(Account account) { updateDB(account); } @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存 public void reload() { reloadAll() } @Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(userName); }
Anotação @CachePut
@CachePut: esta anotação pode ser usada quando é necessário atualizar o cache sem interferir na execução do método. Ou seja, o método sempre é executado, e o resultado é colocado no cache;
| Parâmetros | Explicação | exemplo |
| value | Nome do cache, definido no arquivo de configuração do spring ; é obrigatório especificar pelo menos um | @CachePut(value=”my cache”) |
| key | A chave do cache pode ficar vazia; se for especificada, deverá ser escrita como uma expressão SpEL; se não for especificada, por padrão será formada pela combinação de todos os parâmetros do método | @CachePut(value=”testcache”,key=”#userName”) |
| condição | Condição de armazenamento em cache, pode ficar vazia, é escrita em SpEL e retorna true ou false; o armazenamento em cache só ocorre quando o resultado é true | @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
A anotação @CachePut garante que o método seja executado e que seu valor de retorno também seja registrado no cache, permitindo a atualização sincronizada do cache e do banco de dados.
@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存 public Account updateAccount(Account account) { return updateDB(account); }
Anotação @Caching
Ao usar o cache, pode ser necessário atualizar e excluir ao mesmo tempo, o que pode exigir o uso simultâneo de várias anotações. E @Caching permite fazer isso
//添加user缓存的同时,移除userPage的缓存 @Caching( put =@CachePut(value = "user",key ="#userVo.id"), evict = @CacheEvict(value = "userPage",allEntries = true) )
@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User save(User user) {
@CacheConfig
Todas as anotações @Cacheable()têm um atributo value=“xxx”; evidentemente, quando há muitos métodos, escrever isso fica bastante cansativo. Seria mais prático declarar tudo de uma só vez. Por isso existe a configuração @CacheConfig: @CacheConfig é uma anotação no nível da classe que permite compartilhar os nomes dos caches; se você escrever outro nome no método, o nome especificado no método continuará tendo prioridade.
@CacheConfig("books") public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} }
Cache condicional
A seguir, alguns exemplos comuns de cache condicional
//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反) @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存; @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user)
Anotação de cache personalizada
Por exemplo, a combinação com @Caching mencionada antes pode fazer com que as anotações no método deixem o código um tanto desorganizado. Nesse caso, podemos usar uma anotação personalizada para reunir essas anotações em uma só, como:
@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface UserSaveCache { }
Assim, basta usar o código a seguir no método, deixando o código como um todo mais limpo.
@UserSaveCache public User save(User user)
Extensões
Por exemplo, ao executar findByUsername, não se deve inserir apenas username–>user, mas também id—>user e email—>user; assim, na próxima busca por id, haverá uma correspondência direta no cache
@Caching( cacheable = { @Cacheable(value = "user", key = "#username") }, put = { @CachePut(value = "user", key = "#result.id", condition = "#result != null"), @CachePut(value = "user", key = "#result.email", condition = "#result != null") } ) public User findByUsername(final String username) { System.out.println("cache miss, invoke find by username, username:" + username); for (User user : users) { if (user.getUsername().equals(username)) { return user; } } return null; }
Na verdade, para: id—>user; username—->user; email—>user; uma abordagem melhor talvez seja: id—>user; username—>id; email—>id; garantindo que user seja armazenado uma única vez; por exemplo:
@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username") public void save(User user) @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())") public User findByUsername(String username)
Dados de contexto do SpEL
O Spring Cache fornece alguns dados de contexto SpEL que podemos usar; a tabela abaixo foi extraída diretamente da documentação oficial do Spring:
| Nome | Localização | Descrição | Exemplo |
| methodName | Objeto root | Nome do método que está sendo chamado | root.methodName |
| método | Objeto root | Método que está sendo chamado no momento | root.method.name |
| target | Objeto root | Objeto de destino que está sendo chamado no momento | root.target |
| targetClass | Objeto root | Classe do objeto de destino que está sendo chamado | root.targetClass |
| args | Objeto root | Lista de parâmetros do método que está sendo chamado | root.args[0] |
| caches | Objeto root | A lista de caches usada pela chamada do método atual (por exemplo, @Cacheable(value={“cache1”, “cache2”})), que nesse caso contém dois caches | root.caches[0].name |
| nome do argumento | Contexto de execução | Parâmetros do método que está sendo chamado;por exemplo, em findById(Long id), podemos obter o parâmetro por meio de #id | user.id |
| resultado | Contexto de execução | Valor retornado após a execução do método (válido apenas quando a condição é avaliada após a execução do método, como em ‘unless', 'cache evict' com beforeInvocation=false) | resultado |
@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true) public void conditionUpdate(User user)
Resumo
Para declarar o cache, o mecanismo de cache do Spring oferece um conjunto de anotações Java:
· @Cacheable
o
Função:acionar a gravação no cache; se o dado não estiver no cache, consultar o banco de dados, armazená-lo no cache e retornar o resultado; se estiver no cache, retornar o resultado diretamente
o Aplicação:Consultar o banco de dados Método
· @CacheEvict
o
Função:acionar a limpeza do cache
o Aplicação:Excluir ou modificar o banco de dados Método
· @CachePut
o
Função: gravação no cache (não afeta a execução do método).
o Aplicação:Adicionar ao banco de dados Método
· @Armazenamento em cache
o
Função: recombinar várias operações de cache a serem aplicadas ao método
o Aplicação: uso combinado das anotações acima