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 缓存的获取过程进行记录。

相关推荐
Jia ming2 小时前
ARM多核处理器缓存一致性全解析
arm开发·缓存
dabidai2 小时前
JSR-250JavaEE规范
java
Jackson@ML2 小时前
2026最新版IntelliJ IDEA安装使用指南
java·ide·intellij-idea
逍遥德2 小时前
函数式编程 Java Lambda Stream及其实现类常用函数
java·后端·spring
2501_941982052 小时前
Java 分布式环境下的 Access_Token 一致性方案:如何避免多节点冲突?
java·开发语言·分布式
历程里程碑2 小时前
哈希3 : 最长连续序列
java·数据结构·c++·python·算法·leetcode·tornado
chilavert3182 小时前
技术演进中的开发沉思-328 JVM:垃圾回收(上)
java·开发语言·jvm
椰羊~王小美2 小时前
前后端 格式化货币的方法
java·前端