关闭

Ehcache注脚核心逻辑源码学习

565人阅读 评论(0) 收藏 举报
分类:
Ehcache注解核心逻辑源码学习

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就不需要这样的方式了

0
0
查看评论

spring-boot整合redis作为缓存(4)——spring-boot引入Redis

分几篇文章总结spring-boot与Redis的整合         1、redis的安装         2、redis的设置   &...
  • guduyishuai
  • guduyishuai
  • 2017-04-14 22:32
  • 994

重写spring源码达成场景需求,完美升级

在设计分布式缓存中间件时,提出装卸都不影响工程,也不影响业务代码,也就是零侵入预期,在v1版本做到了封装和继承cacheable等标签做法,但这个被本人不久被推翻,后有制造用逻辑语在cacheName上实现,虽然可用,也不影响,但可读性很差,然而spring源码标签的确在这时不管用,因此只能重写sp...
  • luozhonghua2014
  • luozhonghua2014
  • 2017-03-02 21:41
  • 248

Spring 3.1 中使用 @Cacheable 实现缓存

在软件开发中使用缓存已经有一个非常久的历史了。缓存是一种很好的设计思想,一旦你用了他,你将会发现他确实很有用。Spring3.1版本的核心对缓存做了实现。在Java推出Annotation特性之前,实现缓存的一个难点在于它与业务逻辑代码的耦合性太强。然而,Spring3.1中使用@Cacheable...
  • zly9923218
  • zly9923218
  • 2016-08-02 14:27
  • 494

内存缓存GuavaCache源码解析

1、前言Guava Cache是一个全内存的本地缓存,它提供了线程安全的实现机制。其简单易用、性能好是本地缓存的不二之选。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地Guava Ca...
  • pistolove
  • pistolove
  • 2017-03-05 20:25
  • 1136

EHCache的学习

从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。Spring Cache是作用在方法上的,其核心思想是这样的:当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,...
  • tanyunlong_nice
  • tanyunlong_nice
  • 2016-11-01 15:07
  • 321

Spring+EhCache缓存实例(详细讲解+源码下载)

http://www.importnew.com/15141.html 一、ehcahe的介绍 EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主...
  • zhousenshan
  • zhousenshan
  • 2016-06-27 08:10
  • 168

github上优秀的源码

以下内容来自转载(非常感谢此文第一作者): 一、ListView 二、ActionBar 三、Menu 四、ViewPager 、Gallery 五、GridView 六、ImageView 七、ProgressBar 八、其他 GitHub上优秀Android...
  • qq_33765907
  • qq_33765907
  • 2016-11-02 10:58
  • 6321

啃啃老菜:Spring IOC核心源码学习(一)

啃啃老菜:Spring IOC核心源码学习(一)   本文主要以spring ioc容器基本代码骨架为切入点,理解ioc容器的基本代码组件结构,各代码组件细节剖析将放在后面的学习文章里。   关于IOC容器 IoC容器:最主要是完成了完成对象的创建和依赖的管理注入等等。...
  • novelly
  • novelly
  • 2014-02-20 20:34
  • 676

Ehcache缓存入门实战(附源码)

Ehcache缓存入门实战(附源码)Ehcache是一个强大、成熟的Java缓存框架,可以非常容易与其他库和框架集成。官网声称Ehcache可以作为TB级别的数据缓存,并且具有高扩展性。目前Ehcache的最新版本是3.1。本文不会深究缓存背后的原理,只是作为一个使用Ehcache的例子,由于最近需...
  • u011116672
  • u011116672
  • 2016-08-11 16:49
  • 3506

Spring缓存---创建第一个缓存应用程序

介绍缓存是一种存储机制,它将数据保存在某个地方,并以一种更快的方式为日后的请求提供服务,在应用程序中使用缓存机制,可以避免方法的多次执行,可以根据所提供的输入值对方法的结果数据进行缓存 通过使用AOP原则,对方法进行编织,如果已经为提供的参数执行过,那么不必执行实际方法就可以返回被缓存的结果,所以...
  • jyxmust
  • jyxmust
  • 2017-02-15 18:52
  • 409
    个人资料
    • 访问:723762次
    • 积分:7600
    • 等级:
    • 排名:第3387名
    • 原创:61篇
    • 转载:253篇
    • 译文:3篇
    • 评论:58条
    最新评论