Ehcache支持方法注解方式管理缓存,使代码的侵入性很小。最常用的Ehcache注解是@Cacheable、@CacheEvict、@CachePut
本文通过分析源码整理了这几个注解的实现逻辑和关系,并指出一些组合使用时的限制
1注解类源码
1.1@Cacheable
/** * Cacheable注解缓存方法(或类的所有方法)返回值,缓存Key是方法本身和参数的组合签名 * 通俗说就是同样的参数调用两次,不会重复执行方法,也是缓存最朴素的动机 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Cacheable { /** * 缓存bean的名称,可在xml中定义,支持多个。必填属性 */ String[] value(); /** * SpEL语法表述的缓存键值,默认空表示所有参数的联合签名 * Exp "#arg1" */ String key() default ""; /** * SpEL语法表述的缓存命中条件,默认空没有条件 * Exp "#arg1>0" * condition是在方法调用前判断的,只有命中条件的调用才会触发缓存逻辑,否则方法会被直接调用 * 注意condition只判断是否启用缓存,而不是是否入缓存 * 注意如果条件中带有返回值#result,则无论改调用是否已经在缓存中(通过其他途径),方法仍会被调用,这个后续会说到 * 因此@Cacheble注解的condition条件实际上不能带#result,否则这个缓存相当于失效 * 注意如果方法抛异常,也不会入缓存(因为没有返回值) */ String condition() default ""; /** * SpEL语法表述的缓存否决条件,否决指不入缓存。默认空没有条件 * unless是在方法调用后判断的,因此可以拿到返回值#result,unless判断为true时结果不入缓存 */ String unless() default ""; }
1.2@CachePut
缓存更新注解,不会影响方法调用,并且返回值会更新到缓存。用于缓存的刷新。同一方法@Cacheable和@CachePut不应同时使用
参数与@Cacheable一样,不同点:
condition命中表示更新生效,否则返回值不会更新,方法仍会调用
unless命中表示否决更新,方法仍然调用
由于方法始终调用,单独使用@CachePut时,condition和unless的逆是等价的
1.3@CacheEvict
缓存清空注解,不会影响方法调用,只影响数据出缓存,参数异同:
key参数是有默认值的,默认空表示任何参数调用都清缓存
condition命中表示不清除缓存
没有unless参数
/** * true表示任何调用都清空所有缓存,如设为true,则key参数失效) */ boolean allEntries() default false; /** * true表示方法调用前清缓存,不受返回值及抛异常影响,这是唯一一个在异常情况下仍然生效的注解 * false表示方法调用后清缓存,如果抛异常则不清缓存,如命中@Cacheable也不清缓存 */ boolean beforeInvocation() default false;
2.调用栈
所以Ehcache用的标准spring aop动态代理实现方法拦截,所以Ehcache注解只能加在Spring托管的bean方法上,这一点对于Spring注解都是一样的
核心逻辑在CacheInterceptor这个类(及其父类CacheAspectSupport)
3 CacheInterceptor源码(删掉了一些无关紧要的代码,下同)
/** * 标准的Aop MethodInterceptor实现缓存逻辑,物理缓存为org.springframework.cache.Cache * 缓存逻辑主要在父类中 */ public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable { @Override public Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); // 父类中声明的内嵌接口,实现就是触发invocation原始方法执行 // 之所以要封装一层接口是为了Spring的动态代理可以嵌套,因为cglib代理后是final类,无法直接二次代理 Invoker aopAllianceInvoker = new Invoker() { public Object invoke() { try { return invocation.proceed(); } catch (Throwable ex) { throw new ThrowableWrapper(ex); } } }; // 主方法就是触发父类的execute try { return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments()); } catch (ThrowableWrapper th) { throw th.original; } } }
4 CacheAspectSupport源码
4.1 主要属性
// 常量 private static final String CACHEABLE = "cacheable"; private static final String UPDATE = "cacheupdate"; private static final String EVICT = "cacheevict"; // SpEL语言解析器,处理condition、key、unless等表达式 private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); // 物理缓存容器 private CacheManager cacheManager; // 逻辑缓存容器,此处实现类为AnnotationCacheOperationSource,主要属性为Map<Object, Collection<CacheOperation>> attributeCache // Key是方法签名,CacheOperation可以理解为Ehcache注解的对象化类 // 整个cacheOperationSource封装了全局的缓存注解分布状态 private CacheOperationSource cacheOperationSource; // 缓存Key生成器 private KeyGenerator keyGenerator = new DefaultKeyGenerator();
4.2.主方法
protected Object execute(Invoker invoker, Object target, Method method, Object[] args) { // 这里是为了获取被代理的原始类,也就是@Component的Bean本类 // 此处会剥离其他的Aop增强类以及cglib代理子类 Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target); if (targetClass == null && target != null) { targetClass = target.getClass(); } // 此处获取具体方法的缓存设置,可以理解为method上打的注解全集 Collection<CacheOperation> cacheOp = getCacheOperationSource().getCacheOperations(method, targetClass); // 如果没有打注解,就是普通方法,直接proceed就行了 if (!CollectionUtils.isEmpty(cacheOp)) { // 此步对注解类进一步分类封装,Key就是固定的3个常量,CacheOperationContext是内嵌类,封装了注解本身和物理Cache及相应方法 Map<String, Collection<CacheOperationContext>> ops = createOperationContext(cacheOp, method, args, target, targetClass); // 以下开始为核心逻辑,按一定顺序处理各类缓存注解,此部分逻辑直接影响到注解的使用方式和组合效果 // 首先处理@CacheEvict的beforeInvoke。这个方法和inspectAfterCacheEvicts实际只会生效一个 // 此处是唯一在方法调用前可能对缓存执行写操作的方法 inspectBeforeCacheEvicts(ops.get(EVICT)); // 然后是@Cacheable,这个CacheStatus封装了返回值(缓存or调用方法)和需要更新的缓存值 // 注意此方法还不会执行写缓存操作,而是先记录在status里面,最后一起更新 CacheStatus status = inspectCacheables(ops.get(CACHEABLE)); Object retVal; // 然后是@CachePut,这个注解不会影响方法调用 // 这个Map封装了所有待更新的key,注意key可能有多个,retVal只会有一个 Map<CacheOperationContext, Object> updates = inspectCacheUpdates(ops.get(UPDATE)); // 最终处理之前的结果 if (status != null) { // 如果@Cacheable有未命中缓存的调用,添加到待更新的key集合,注意此处仍未执行写缓存,也没有调用方法 if (status.updateRequired) { updates.putAll(status.cacheUpdates); } else { // 如果命中了缓存,直接返回缓存的数据,整个处理结束 // 注意一旦命中缓存,除了@CacheEvict(beforeInvocation=true),不会对缓存进行其他任何写操作 return status.retVal; } } // 走到这里,说明没有@Cacheable护照缓存没有命中,于是需要调用方法proceed retVal = invoker.invoke(); // 拿到返回值后,再处理@CacheEvict(beforeInvoke=false),逻辑基本与inspectBeforeCacheEvicts一样,只是condition可以带#result inspectAfterCacheEvicts(ops.get(EVICT), retVal); // 最后统一写缓存 if (!updates.isEmpty()) { // 此处会解析@CachePut和@Cacheble的unless属性生效,会判断是否命中unless,如命中则最终不会写缓存 update(updates, retVal); } return retVal; } return invoker.invoke(); }
4.3.Evict
private void inspectBeforeCacheEvicts(Collection<CacheOperationContext> evictions) { // 调用前,没有返回值,所以返回值传ExpressionEvaluator.NO_RESULT标志位 inspectCacheEvicts(evictions, true, ExpressionEvaluator.NO_RESULT); } private void inspectAfterCacheEvicts(Collection<CacheOperationContext> evictions, Object result) { inspectCacheEvicts(evictions, false, result); } // 实现方法,result为原始方法的返回值 private void inspectCacheEvicts(Collection<CacheOperationContext> evictions, boolean beforeInvocation, Object result) { if (!evictions.isEmpty()) { for (CacheOperationContext context : evictions) { // 枚举每个@CacheEvict CacheEvictOperation evictOp = (CacheEvictOperation) context.operation; // 这个if根据beforeInvocation参数判断是方法调用前还是调用后生效 if (beforeInvocation == evictOp.isBeforeInvocation()) { // 这个地方过滤condition是否命中,不命中的话注解无效 // 注意如果ExpressionEvaluator.NO_RESULT (beforeInvocation),且SpEL含有#result,这个条件是必不过的 // 所以在如果beforeInvocation,condition又写了#result,相当于没有写condition if (context.isConditionPassing(result)) { // 缓存key是lazy load的,第一个Cache操作的时候才生成 Object key = null; for (Cache cache : context.getCaches()) { // 这个if就是@CacheEvict的allEntries参数 if (evictOp.isCacheWide()) { cache.clear(); // 如果allEntries=true,清空整个缓存 } else { // 否则就清除单个Key if (key == null) { key = context.generateKey(); } cache.evict(key); } } } } } } }
4.4.Cache
private CacheStatus inspectCacheables(Collection<CacheOperationContext> cacheables) { // 待写缓存的内容 Map<CacheOperationContext, Object> cacheUpdates = new LinkedHashMap<CacheOperationContext, Object>(cacheables.size()); boolean cacheHit = false; // 是否命中缓存,如最终为true不调用方法proceed Object retVal = null; if (!cacheables.isEmpty()) { // 是否存在某个condition通过,同一方法不能打两个@Cacheable,这里多个可能是考虑到其他aop的情况 boolean atLeastOnePassed = false; for (CacheOperationContext context : cacheables) { // 过滤condition是否命中,不命中的话注解无效(会执行方法) // 注意如果condition含有#result,这个条件是必不过的,相当于注解失效 if (context.isConditionPassing()) { atLeastOnePassed = true; Object key = context.generateKey(); // 生成缓存Key if (key == null) { throw new IllegalArgumentException("Null key returned for cache operation (maybe you " + "are using named params on classes without debug info?) " + context.operation); } // 记录所有关联的key,key可能有多个,value只会有一个 cacheUpdates.put(context, key); // check whether the cache needs to be inspected or not (the method will be invoked anyway) if (!cacheHit) { // 这个分支全方法只会进一次 for (Cache cache : context.getCaches()) { Cache.ValueWrapper wrapper = cache.get(key); // 命中缓存 if (wrapper != null) { retVal = wrapper.get(); // 这个分支全方法也只会进一次 cacheHit = true; // 注意如果注册了多个缓存,只取第一个命中的值,而缓存顺序是不能指定的 break; } } } } } // 返回,如果condition(全部)不通过,则返回null,相当于注解失效,否则返回结果 if (atLeastOnePassed) { // 因为cacheHit是全局的,只要有一个缓存命中updateRequired就是false,其他缓存都不会刷新(即使有的值已经过期) // 如果全部没有命中,才会添加返回值到各个缓存 return new CacheStatus(cacheUpdates, !cacheHit, retVal); } } return null; }
4.5.Put
private Map<CacheOperationContext, Object> inspectCacheUpdates(Collection<CacheOperationContext> updates) { Map<CacheOperationContext, Object> cacheUpdates = new LinkedHashMap<CacheOperationContext, Object>(updates.size()); if (!updates.isEmpty()) { for (CacheOperationContext context : updates) { if (context.isConditionPassing()) { // condition通过 Object key = context.generateKey(); if (key == null) { throw new IllegalArgumentException("Null key returned for cache operation (maybe you " + "are using named params on classes without debug info?) " + context.operation); } // Put的逻辑很简单,通过condition就直接添加待更新的key,由上层统一写入 cacheUpdates.put(context, key); } } } return cacheUpdates; }
5.一个应用场景
一个Redis的KV表,上层用Ehcache缓存
要求:Redis获取成功时,返回值入缓存;Redis获取失败时,不入缓存,下次调用仍然执行Redis查询
5.1正确方式
@Override @Cacheable(value = "myCache", key = "#key", unless = "#result<0") public short getActiveTime(String key) { String value = redisDriver.get(key); try { return Short.valueOf(value); } catch (Exception e) { return -1; } }
5.2错误的方式1
@Cacheable(value = "activeTimeCache", key = "#deviceSignature", condition="#result>=0")
错误原因是condition是方法调用前校验,@Cacheable的condition中带有#result,则condition一定不成立,所以注解相当于失效
5.3错误的方式2
@CacheEvict(value = "activeTimeCache", key = "#deviceSignature", condition="#result<0") @Cacheable(value = "activeTimeCache", key = "#deviceSignature")
此写法本意是先全部缓存,再删掉返回值<0的。但是根据之前的源码,@Cacheable的实际写操作要晚于@CacheEvict,所以-1还是会入缓存
以上这种方式如要生效也是可以的,需要做两层嵌套的Bean,把@CacheEvict加在外层方法上,这时候外层的inspectAfterCacheEvicts会晚于内层的update。当然为了缓存加一层嵌套是很不美观的。有了unless就不需要这样的方式了
相关推荐
源码库包含所有类的定义、方法实现以及注释,对于深入学习和定制Ehcache功能非常有帮助。 学习Ehcache时,开发者需要理解其配置文件(通常为`ehcache.xml`),其中包含了缓存管理器的配置、缓存的设置等信息。此外...
总结,通过对Ehcache源码的阅读和修改后的jar包的使用,开发者不仅可以学习到缓存管理的最佳实践,还可以根据项目需求对Ehcache进行深度定制,以提高应用的性能和效率。同时,这也是一种很好的学习和研究Java缓存...
博客链接 "<https://cndubian.iteye.com/blog/2146721>" 可能提供了更详细的实现步骤和示例代码,对于想要学习Spring MVC、Ehcache集成和接口测试的开发者来说是个宝贵的资源。 在"msm_kpi"这个压缩包文件中,可能...
3. **事件驱动**:Ehcache支持缓存事件监听器,可以监听到诸如添加、更新、移除等操作,便于实现缓存同步和其他业务逻辑。 4. **异步操作**:支持异步缓存操作,提高系统响应速度,尤其在高并发环境下。 5. **可...
这是一个基于Java技术栈的仓库管理系统源码,使用了Spring MVC作为MVC框架,Mybatis作为持久层框架,Ehcache作为缓存管理工具,Apache Shiro进行权限控制,以及Bootstrap作为前端UI框架。下面将详细解析这些技术在...
通过阅读这些源码,你可以深入理解Hibernate的工作原理,学习如何实现ORM框架,以及优化数据库访问性能。同时,这也是提升Java开发技能和数据库设计能力的好途径。如果你希望深入学习Hibernate,可以从这些模块入手...
Hibernate是Java领域中一款广泛应用的关系型数据库对象映射...总之,对Hibernate源码的学习不仅可以提升数据库操作的技能,还可以帮助开发者更好地理解和解决实际项目中的问题,实现更高效、更灵活的数据访问层设计。
综上所述,通过学习`hibernate二级缓存示例源码`,我们可以了解到如何在实际项目中配置和使用Hibernate二级缓存,从而提升系统的性能。在实际应用中,应结合具体场景选择合适的缓存策略,以达到最佳的性能优化效果。
Spring框架是Java开发中不可或缺的一部分,它以其IoC(Inversion of Control)和AOP(Aspect-Oriented Programming)的核心特性,以及丰富的模块化...对于想要深入学习Spring的开发者来说,研究源码是不可或缺的步骤。
这个压缩包包含的文件属于Hibernate的核心组件,很可能包括了`org`目录,这个目录下分布着Hibernate的各种核心类和接口。 在Hibernate源码中,我们可以关注以下几个关键知识点: 1. **Entity和Session**:在...
`hibernate源码`包含Hibernate框架的核心实现,对于深入理解其工作原理、优化代码或者进行二次开发至关重要。通过阅读源码,开发者可以学习到以下内容: 1. **Caching机制**: Hibernate支持二级缓存,如Ehcache,...
在本项目"spring-boot-mybatis-cache-thymeleaf学习练习demo源码"中,我们可以深入学习和实践如何将Spring Boot、MyBatis、Cache(通常指的是Spring Cache)以及Thymeleaf这四个关键组件整合在一起,创建一个高效、...
它的源码包包含了整个系统的源代码,便于开发者进行二次开发或者深入学习系统架构和实现方式。在这个压缩包中,我们可以找到以下关键组成部分: 1. **Readme-说明.htm**:这是一个文档,通常包含项目的简介、安装...
在源码学习中,我们可以深入理解Spring Boot如何实现自动配置、如何启动和管理Spring容器、以及如何优雅地集成各种框架和服务。通过对`SpringApplication.run()`方法的追踪,可以了解Spring Boot应用的启动流程。...
项目结构整洁,条理清晰,旨在帮助学习者逐步掌握Hibernate4的核心概念和技术。” 【标签】:“Hibernate4 源码”和“Hibernate4 完整案例”表明了这个压缩包内容是关于Hibernate4框架的原始代码,以及一个完整的...
源码学习是提升技术水平的重要途径,通过对源码的研读,我们可以了解设计模式的应用、框架内部的工作流程以及优化策略。解压密码为“www.cqlsoft.com”,确保了资料的安全性。其中包含的“内容来自存起来软件站...
通过源码学习,开发者可以更好地解决这些问题,例如优化查询、定制化缓存策略、适配不同的数据库等。 总之,深入理解Hibernate源码不仅能够提升编程技巧,也能为解决实际开发中的问题提供理论支持,进一步提高项目...
Spring MVC 是一个基于 Java 的轻量级 Web 开发框架,它是 ...这个"spring mvc项目后端源码"可能包含了上述部分或全部概念的实现,通过学习和分析这些代码,可以加深对 Spring MVC 框架的理解,并提升 Web 开发技能。
本文将深入探讨Hibernate 3.2.1版本的源码,揭示其核心设计理念与实现机制,帮助读者深化对ORM的理解,提升开发技能。 首先,Hibernate 3.2.1是Hibernate系列中的一个重要版本,它在功能、性能以及稳定性上都有显著...