Spring 缓存 @Cacheable 实现原理


文章目录

  • 前言
  • [一、 @Cacheable bean 加载](#一、 @Cacheable bean 加载)
  • 二、缓存查询
    • [2.1 切面通知器进入](#2.1 切面通知器进入)
    • [2.2 缓存查询:](#2.2 缓存查询:)
  • 总结

前言

我们在开发中有时会使用到 @Cacheable 来缓存结果,这样在下一次请求进入后,可以先从缓存中获取结果,如果缓存中没有在进入原方法,最后将获取到的结果放入到缓存中,以便下次请求可以直接使用。那么 @Cacheable 它是如何实现的呢?


一、 @Cacheable bean 加载

spring 在创建单例bean 的时候,有一个步骤就是判断 是否需要为改bean 创建一个代理的bean 来进行业务的增强。

bean 的代理类过程:

  • Spring 启动时,AnnotationAwareAspectJAutoProxyCreator(自动代理创建器)会扫描所有 Bean;
  • 若 Bean 有切面相关注解(如 @Cacheable/@Transactional/@Aspect), 获取当前 Bean 匹配的所有通知(Advice),为其创建动态代理bean;

AbstractAutoProxyCreator,核心方法是 postProcessAfterInitialization(Bean 初始化后处理):

java 复制代码
// AbstractAutoProxyCreator 核心方法(简化版)
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        // 1. 为 Bean 生成唯一键(类名 + beanName)
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 2. 检查是否已创建过代理,避免重复创建
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 3. 核心:判断是否需要创建代理,需要则创建
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

// 核心逻辑:判断是否需要创建代理
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    // 步骤1:获取当前 Bean 匹配的所有通知(Advice)
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    // 步骤2:若有匹配的通知,创建代理
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 步骤3:创建代理对象(核心!)
        Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

二、缓存查询

当访问后端资源时,会先获取到改资源对应的 通知 责任链,然后依次进行调用;本文 中 spring-boot-starter-cache 版本为 4.0.1

2.1 切面通知器进入

ReflectiveMethodInvocation 中的invocation.proceed() 是执行通知链的核心,采用「责任链模式」依次执行每个通知:

java 复制代码
// ReflectiveMethodInvocation.proceed() 核心源码(简化版)
@Override
public Object proceed() throws Throwable {
    // 1. 如果所有通知都执行完了,执行原始方法
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint(); // 调用原始方法:method.invoke(target, args)
    }
    // 2. 获取下一个要执行的通知(如 CacheInterceptor)
    Object interceptor = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 3. 执行当前通知,并递归调用 proceed() 执行后续通知
    if (interceptor instanceof InterceptorAndDynamicMethodMatcher) {
        // 动态匹配的通知(如根据方法参数匹配)
        InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptor;
        if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        } else {
            return proceed(); // 不匹配则跳过,执行下一个通知
        }
    } else {
        // 普通通知:执行通知逻辑(如 CacheInterceptor.invoke())
        return ((MethodInterceptor) interceptor).invoke(this);
    }
}
  • currentInterceptorIndex 记录当前执行到的通知索引;
  • 每个通知执行时,会调用 invocation.proceed() 触发下一个通知的执行;
  • 最后一个通知执行完后,调用 invokeJoinpoint() 执行原始方法 ------ 这就是「环绕通知」能控制是否执行原始方法的底层原因。

缓存切面的通知实现:CacheInterceptor

@Cacheable 的核心是 CacheInterceptor(实现了 MethodInterceptor),其 invoke() 方法就是通知逻辑:

java 复制代码
// CacheInterceptor.invoke() 核心源码
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
    // 调用父类逻辑:先查缓存,未命中则执行原始方法
    return execute(invocation, invocation.getThis(), invocation.getMethod(), invocation.getArguments());
}

2.2 缓存查询:

CacheInterceptor # invoke 进行缓存查询 ,本文以redis 实现为例 ,对应spring-context 为7.0.1

CacheAspectSupport #execute -》 该方法是否有缓存的注解-》有缓存的注解-》findCachedValue 获取缓存中的数据-》如果命中缓存则直接将数据返回-》 如果没有命中缓存则调用原始的方法, 将获取到的数据进行缓存-》 返回数据

(1) 缓存获取:

java 复制代码
@Override
	public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable {
		// 调用的目标方法
		Method method = invocation.getMethod();
		// 封装原始方法执行逻辑:调用 invocation.proceed() 执行业务方法
		CacheOperationInvoker aopAllianceInvoker = () -> {
			try {
				return invocation.proceed();
			}
			catch (Throwable ex) {
				throw new CacheOperationInvoker.ThrowableWrapper(ex);
			}
		};

		Object target = invocation.getThis();
		Assert.state(target != null, "Target must not be null");
		try {
			// 用父类 CacheAspectSupport 的 execute 方法
			return execute(aopAllianceInvoker, target, method, invocation.getArguments());
		}
		catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}

execute 获取缓存数据

bash 复制代码
private @Nullable Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// sync 同步注解处理
		if (contexts.isSynchronized()) {
			// Special handling of synchronized invocation
			return executeSynchronized(invoker, method, contexts);
		}

		// Process any early evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Check if we have a cached value matching the conditions
		// 获取缓存数据
		Object cacheHit = findCachedValue(invoker, method, contexts);
		if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) {
			// 没有命中缓存 则调用原始的方法并获取结果数据后 放入到缓存中
			return evaluate(cacheHit, invoker, method, contexts);
		}
		return cacheHit;
	}

private @Nullable Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
			// 遍历有缓存的操作
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				// 生成缓存对应 的key
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				// 获取缓存数据
				Object cached = findInCaches(context, key, invoker, method, contexts);
				if (cached != null) {
					// 命中缓存数据则直接返回
					if (logger.isTraceEnabled()) {
						logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames());
					}
					return cached;
				}
				else {
					if (logger.isTraceEnabled()) {
						logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
					}
				}
			}
		}
		// 没有名中缓存数据则返回null
		return null;
	}
	

(2) 没有名中缓存

当从缓存中获取的数据为null 时,则调用原始方法,获取数据,并进行缓存

bash 复制代码
private @Nullable Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method,
			CacheOperationContexts contexts) {

		// Re-invocation in reactive pipeline after late cache hit determination?
		if (contexts.processed) {
			return cacheHit;
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = unwrapCacheValue(cacheHit);
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// Invoke the method if we don't have a cache hit
			// 通过反射调用原始的方法
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect puts from any @Cacheable miss, if no cached value is found
		List<CachePutRequest> cachePutRequests = new ArrayList<>(1);
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
		}

		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			// 最终调用 doPut(cache, key, value); 将结果缓存
			Object returnOverride = cachePutRequest.apply(cacheValue);
			if (returnOverride != null) {
				returnValue = returnOverride;
			}
		}

		// Process any late evictions
		Object returnOverride = processCacheEvicts(
				contexts.get(CacheEvictOperation.class), false, returnValue);
		if (returnOverride != null) {
			returnValue = returnOverride;
		}

		// Mark as processed for re-invocation after late cache hit determination
		contexts.processed = true;
		// 返回结果数据
		return returnValue;
	}

总结

本文对Cacheable 缓存的获取过程进行记录。

相关推荐
IT枫斗者枫哥6 分钟前
Java 文件导出:写完了,为什么还不能标记成功?
java
xixingzhe26 分钟前
SpringBoot DDD 实战入门文档
spring boot·spring·ddd
陈皮波比茶12 分钟前
java笔记-typora快捷键
java
蚂蚁雅嘿20 分钟前
maven标签日常总结
java·maven
洋不写bug32 分钟前
二叉树(二) 常见基础操作解析|结点数、树高、查找结点、判断完全二叉树
java·开发语言·数据结构·完全二叉树·二叉树结点数·树高·查找结点
菠萝加点糖42 分钟前
Maven 坐标系统说明
java·maven
hm宋1 小时前
Canal 升级 jar 包操作文档(以升级 FastJSON 为例)
java·canal·jar
后台模板学习1 小时前
学习的心态高频面试题
java·数据库·学习
用户3126874877201 小时前
线程池到底怎么管理线程的?从 ThreadPoolExecutor 到拒绝策略全链路拆解
java
cpolar技术支持2 小时前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams