Spring 3.1+ 引入抽象緩存(@Cacheable 等),底層基於 AOP 切面攔截。本文從零手寫一個 @MyCacheable 註解及其處理切面,深度理解緩存鍵生成、切面編織、緩存存儲全流程。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCacheable {
// 可擴展:value 指定緩存名稱、key 自定義 SpEL 表達式等
}
@Aspect
public class CacheAspect {
private final Map<String, Object> cache = new HashMap<>(); // 生產環境換 Redis/Caffeine
private final Logger log = LoggerFactory.getLogger(CacheAspect.class);
@Pointcut("@annotation(MyCacheable)")
public void cacheableMethods() {}
@Around("cacheableMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1. 生成緩存 Key:類名.方法名(參數類型=值;...)
StringBuilder key = new StringBuilder();
key.append(pjp.getTarget().getClass().getName())
.append(".").append(pjp.getSignature().getName())
.append("(");
for (Object arg : pjp.getArgs()) {
key.append(arg.getClass().getSimpleName())
.append("=").append(arg).append(";");
}
key.append(")");
String cacheKey = key.toString();
// 2. 查緩存
Object cached = cache.get(cacheKey);
if (cached != null) {
log.debug("Cache hit: {}", cacheKey);
return cached;
}
// 3. 執行目標方法
log.debug("Cache miss, executing: {}", cacheKey);
Object result = pjp.proceed();
// 4. 存入緩存
cache.put(cacheKey, result);
log.info("Cached result: {} -> {}", cacheKey, result);
return result;
}
}
// Calculator.sum(1, 2) → caching.springaop.Calculator.sum(Integer=1;Integer=2;)
public class Calculator {
private static final Logger log = LoggerFactory.getLogger(Calculator.class);
@MyCacheable
public int sum(int a, int b) {
log.info("Calculating {} + {}", a, b);
try { Thread.sleep(3000); } catch (InterruptedException e) { /* ignore */ }
return a + b;
}
}
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xsi:schemaLocation="...">
<aop:aspectj-autoproxy />
<bean class="caching.springaop.CacheAspect" />
<bean id="calc" class="caching.springaop.Calculator" />
</beans>
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
Calculator calc = ctx.getBean("calc", Calculator.class);
log.info("1 + 2 = {}", calc.sum(1, 2)); // 執行 3 秒,存入緩存
log.info("1 + 2 = {}", calc.sum(1, 2)); // 瞬時返回,命中緩存
}
}
日誌輸出:
Calculating 1 + 2
1 + 2 = 3
1 + 2 = 3 // 無 Calculating 日誌,直接返回緩存值
| 維度 | 建議替換 |
|---|---|
| 緩存存儲 | HashMap → Redis(分佈式)、Caffeine(本地高性能) |
| Key 生成 | 硬編碼 → SpEL 表達式(@Cacheable(key="#root.methodName")) |
| 過期策略 | 無 → TTL、LRU、@CacheEvict 主動清理 |
| 序列化 | 默認 → JSON/Protobuf 跨語言 |
手寫 @MyCacheable 切面揭示了 Spring 緩存抽象的核心機制:註解標記 → 切面攔截 → Key 生成 → 緩存查/存 → 返回目標結果。理解此流程,有助於排查緩存不生效、Key 衝突、序列化失敗等生產問題。