Cache注解
- 开启注解,以后再方法上加个注解就可以自动把查询到的数据放到缓存中。
如果是用xml配置文件创建CacheManager的话,添加以下配置即开启cache注解:
<!--假设上面注册了spring的CacheManager,id为cacheManager--> <cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/> <!--还有一个属性key-generator,即key生成策略,默认是SimpleKeyGenerator-->
如果是Java configuration的话:
@Configuration
@ComponentScan(basePackages = "com.haien.sping.cache.service")
@EnableCaching(proxyTargetClass = true) //效果同<cache:annotation-driven/>,开启注解
public class AnnotationCacheConfig {
@Bean
public CacheManager cacheManager(){
try {
//2.
net.sf.ehcache.CacheManager ehcacheCacheManager=new net.sf.ehcache.CacheManager(
new ClassPathResource("ehcache.xml").getInputStream()); //1.
//3.
EhCacheCacheManager cacheCacheManager=new EhCacheCacheManager(ehcacheCacheManager);
return cacheCacheManager;
} catch (IOException e) { //getInputStream()抛出的
throw new RuntimeException(e);
}
}
}
- 如果想要设置KeyGenerator的话,可以实现CachingConfigurer:
@Configuration
@ComponentScan(basePackages = "com.haien.sping.cache.service")
@EnableCaching(proxyTargetClass = true)
public class AnnotCacheConfWithKeyGenertor implements CachingConfigurer {
@Bean
public CacheManager cacheManager() {
try {
net.sf.ehcache.CacheManager ehcacheCacheManager=new net.sf.ehcache.CacheManager(
new ClassPathResource("ehcache.xml").getInputStream()
);
EhCacheCacheManager cacheCacheManager=new EhCacheCacheManager(ehcacheCacheManager);
return cacheCacheManager;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//默认为此
@Bean
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
}
- 然后使用cache注解:所有注解都可以放在类或方法上,如果放在类上则会应用到所有方法上;并且,如果将注解放在接口类的方法上,则所有实现类在重写方法时都会继承这个注解。
@Service
public class UserService {
//假设这是数据库
Set<User> users=new HashSet<User>();
//调用该方法时,会把user.id作为key,放入value指定的缓存,可以指定多个缓存,
如,value={"user1","user2"};#表示调用参数
@CachePut(value = "user",key = "#user.id")
public User save(User user){
users.add(user);
return user;
}
//调用方法前先从缓存中读取,没有再调用方法并把数据放进缓存
@Cacheable(value = "user",key="#id")
public User findById(final Long id){
System.out.println("cache miss,invoke find by id,id="+id);
for(User user:users){
if(user.getId().equals(id))
return user;
}
return null;
}
@CacheEvict(value = "user",key = "#user.id") //移除指定key的数据
public void delete(User user){
users.remove(user);
}
@CacheEvict(value = "user",allEntries = true) //移除所有数据
public void deleteAll(){
users.clear();
}
}
- @CachePut:应用在写数据的方法上,在调用方法前并不会先检查缓存中是否已有该值,即有也会被覆盖。
public @interface CachePut {
String[] value(); //缓存的名字,可以把数据放入多个缓存;支持SpEL表达式
String key() default ""; //缓存key,不指定将使用默认的KeyGenerator生成;SpEL
String condition() default ""; //能否放入缓存的条件,在方法调用前后都会判断;
SpEL
String unless() default ""; //返回false则能放入缓存;SpEL
}
- @Cacheable: 应用在读数据的方法上,即可缓存的方法上,如查找;调用前会先从缓存读取,没有再调用方法,并把数据放入缓存。
public @interface Cacheable {
String[] value(); //同@CachePut
String key() default ""; //同@CachePut
String condition() default ""; //方法调用前判断是否能从缓存拿;
执行方法后是否能把数据放入缓存的条件
String unless() default ""; //返回false则能放入缓存
}
- @CacheEvict:应用在移除数据的方法上。
public @interface CacheEvict {
String[] value(); //同@CachePut
String key() default ""; //同@CachePut
String condition() default ""; //调用前还是调用后判断取决于beforeInvocation
boolean allEntries() default false; //是否移除所有数据
boolean beforeInvocation() default false;//是调用方法前还是调用后移除
}
注解参数所用的SpEL
target:被注解方法所在的类
通过这些数据我们可以实现比较复杂的缓存逻辑了。
条件缓存
- 利用注解的condition或unless属性,进行有条件的缓存。
- @Cacheable:在方法调用前判断condition,返回true则先从缓存中拿:
@Cacheable(value = "user", key = "#id", condition = "#id lt 10") //id<10?
public User conditionFindById(final Long id)
//condition示例:以下代码位于UserService类中
//判断执行前是否要先删缓存
@CacheEvict(value = "user",key = "#user.id",
condition = "#root.target.canEvict(#root.caches[0],#user.id,#user.username)",
beforeInvocation = true)
public void conditionalUpdate(User user){
users.remove(user);
users.add(user);
}
/**
* @Author haien
* @Description 判断是否需要删除缓存
* @Date 2019/3/24
* @Param [userCache名为“user”的cache, id, username]
* @return boolean
**/
public boolean canEvict(Cache userCache,Long id,String username) {
//根据id查出条目
User user=userCache.get(id,User.class);
//查不到则不用删缓存
if(user==null)
return false;
//查到了但username不是指定的那个也不用删除
return !user.getUsername().equals(username);
}
- 以上方法缺点在于需要将缓存判断方法也暴露出去,而且缓存代码和业务代码混在一起,所以把canEvict()移到一个Helper静态类中更好(不过代码中没实现):
@CacheEvict(value = "user", key = "#user.id",
condition = "T(com.haien.service.UserCacheHelper)
.canEvict(#root.caches[0], #user.id, #user.username)",
beforeInvocation = true)
public void conditionUpdate(User user){}
- @CachePut:以下condition需要result值,所以只在方法执行后才判断,返回true则放入缓存:
//ne:应该是not equal的意思
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")
public User conditionSave(final User user)
- 如下注解只在方法执行后才判断unless,返回false才放入缓存:
@CachePut(value = "user", key = "#user.id",
unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user)
//message是result的属性,contains是SpEL的方法
@Cacheable(value = "spittleCache",
unless = "#result.message.contains('NoCache')")
Spittle findOne(long id);
- @CacheEvict:beforeInvocation=false表示在方法执行后才删除缓存,且condition要返回true才删除:
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false,
condition = "#result.username ne 'zhang'")
public User conditionDelete(final User user)
@Caching
- 用于组合多个注解使用。
public @interface Caching {
Cacheable[] cacheable() default {}; //声明多个@Cacheable
CachePut[] put() default {}; //声明多个@CachePut
CacheEvict[] evict() default {}; //声明多个@CacheEvict
}
- 比如,新增用户成功后,我们要分别以id、username和email为key同时存储value都为user实例的三条数据,这时可以使用该注解:
@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) {}
- 其实最好是id-user,然后username-id、email-id,保证user只存一份。
运行流程
- 使用@Caching组合多个注解或在同个方法加多个注解时,运行流程如下:
- 首先执行@CacheEvict清空缓存(如果beforeInvocation=true且condition通过)。
- 接着收集@Cacheable(如果condition通过,即方法执行后允许写入缓存,且key对应的数据不在缓存);
- 然后收集@CachePut(如果condition通过)。
- 全部收集到cachePutRequests(表示缓存中缺失数据,有放入缓存的需求)。
- 如果cachePutRequests为空,那么@Cacheable时将先查找缓存,否则直接开始执行方法。
- 如果缓存中找不到数据,那么开始执行方法,把结果放入result。
- 执行cachePutRequests,把result中的数据写入缓存(如果unless返回false)。
- 执行@CacheEvict(如果beforeInvocation=false 且 condition 通过)。
- 其实也就是有beforeInvocation或condition这种需要在方法执行前先检查的注解先被处理,然后再执行方法,最后再处理有beforeInvocation或condition的注解。
- 由以上2、3步,只要注解中有@CachePut,cachePutRequests就不可能为空,@Cacheable就不会去缓存中取。
解决@Cacheable和@CachePut不能同时用的问题
- 只要有@CachePut在,@Cacheable就不会从缓存拿的问题其实只要改掉spring中CacheAspectSupport类某个方法的判断条件即可。
- 我们在自己的test包下建包:org.springframework.cache.intercepter(CacheAspectSupport原本就在spring框架中的该包下),自己一模一样的CacheAspectSupport类,只修改其中判断能否从缓存查东西的条件(本来是只要有@CachePut就不查,现在改成只要有2Cacheable就查):
package org.springframework.cache.interceptor
public abstract class CacheAspectSupport implements InitializingBean {
private Object execute(Invoker invoker, CacheOperationContexts contexts) {
...
Collection<CacheOperationContext> cacheOperationContexts =
contexts.get(CacheableOperation.class);
/* 原代码要求cacheOperationContexts为空且cachePutRequests也为空,即没有@CachePut时才从缓存拿
if (cachePutRequests.isEmpty() && cacheOperationContexts.isEmpty()) {
result = findCachedResult(cacheOperationContexts);
}*/
//现在只查cacheOperationContexts,且相反,不为空才查缓存,也就是只要有@Cacheable就查缓存
if (!cacheOperationContexts.isEmpty()) {
result = findCachedResult(cacheOperationContexts);
}
...
}
}
- 但是只改上面这个类只是做了第一步,后面还要做什么配置就不知道了。
自定义组合注解
- 上面那个@Caching用在方法上会显得有点乱,此时我们可以把它定义成一个简洁的注解:
@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 {
}
KeyGenerator
- KeyGenerator接口:如果在Cache注解上没有指定key的话,如@CachePut(value=”user”),会调用KeyGenerator自动生成一个key。
public interface KeyGenerator {
Object generate(Object target, Method method, Object... params);
}
- 默认实现:
- DefaultKeyGenerator:
@Override
public Object generate(Object target, Method method, Object... params) {
//如果参数为空,就不知道key是个什么特定值了
if (params.length == 0) {
return SimpleKey.EMPTY;
}
//如果只有一个参数,就使用参数作为key,即传入User实例名为User参数
的方法,key默认为“user”
if (params.length == 1 && params[0] != null) {
return params[0];
}
return new SimpleKey(params);
}
- SimpleKeyGenerator:Spring4之后默认使用的。
- 自定义KeyGenerator:然后用<cache:annotation-driven key-generator=””/>指定,或实现CachingConfiguration接口时重写方法设置进去。
使用xml代替缓存注解
- 提供元素如下:
缓存模糊匹配
- 代码示例:ideaProjects/spring-cache