
Spring 的 @Cacheable 等缓存注解依赖 AOP 代理生效,若在测试中手动 new MyService() 创建实例,将绕过 Spring 容器管理,导致代理失效、缓存不触发。
spring 的 `@cacheable` 等缓存注解依赖 aop 代理生效,若在测试中手动 `new myservice()` 创建实例,将绕过 spring 容器管理,导致代理失效、缓存不触发。
在 Spring 非 Boot 项目中启用声明式缓存(如 @Cacheable)时,核心前提不是仅添加 @EnableCaching 或配置 CacheManager,而是确保被注解的目标方法由 Spring 容器托管的代理对象调用。你的测试失败的根本原因在于:
@Configuration
class TestConfig {
@Bean MyApp myApp() {
return new MyApp(new MyService()); // ❌ 错误:手动 new,脱离容器
}
@Bean CacheManager cacheManager() {
return new ConcurrentMapCacheManager("greeting");
}
}
此处 new MyService() 绕过了 Spring 的 Bean 生命周期管理,MyService 实例未被 CachingAdvisor 增强,因此 @Cacheable("greeting") 完全不生效——第二次调用仍会执行 Thread.sleep(1000),导致测试超时断言失败。
✅ 正确做法是让 Spring 完全托管所有相关 Bean,包括 MyService:
@Configuration
@EnableCaching
@ComponentScan("com.somepkg") // 确保扫描到 @Component 类
static class TestConfig {
@Bean
MyApp myApp(MyService myService) { // ✅ 通过参数注入,由容器提供代理实例
return new MyApp(myService);
}
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("greeting");
}
}
同时,确保 MyService 类被 Spring 扫描并注册为 Bean(你已在 Main 中使用 @ComponentScan,但测试配置中缺失)。推荐显式启用扫描(如上),或直接在 TestConfig 中声明 MyService Bean:
@Bean
MyService myService() {
return new MyService(); // ✅ 此处 new 是允许的,因为 @Bean 方法本身由容器管理,返回值会被自动代理
}
? 关键原理:Spring 对
@Cacheable的支持基于 JDK 动态代理(接口)或 CGLIB 代理(类)。只有当方法调用发生在 Spring 创建的代理对象上时,CacheInterceptor才能拦截调用、检查缓存、跳过实际执行。手动生成的实例无代理,等同于普通 Java 对象调用。
此外,请确认测试类正确启用 Spring 测试上下文:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestConfig.class)
public class SomeTest {
@Autowired MyApp myApp; // ✅ 自动注入,保证使用容器管理的实例
@Test
public void testStuff() {
// 第二次 hello() 将命中缓存,耗时趋近于 0ms
myApp.run("stuff");
myApp.run("stuff"); // ← 此次不再 sleep(1000)
}
}
? 总结与最佳实践:
- 永远避免在
@Bean方法中new出被@Cacheable/@Transactional等 AOP 注解修饰的类; - 优先通过构造函数/方法参数注入依赖,交由 Spring 统一管理生命周期和代理;
- 在测试配置中显式
@ComponentScan或声明所有必要@Bean,确保无遗漏; - 可通过调试验证:断点进入
MyService.hello(),观察this的实际运行时类型是否为MyService$$EnhancerBySpringCGLIB$$xxx(CGLIB 代理)或com.sun.proxy.$Proxyxx(JDK 代理); - 若需单元测试隔离,可考虑
@MockBean(需 Spring Test 支持)或纯 Mockito 测试,但集成测试务必走完整容器流程。
遵循以上改造后,SomeTest 将稳定通过——两次 hello() 调用总耗时显著低于 1100ms,证明缓存已生效。










