在Spring Boot项目中实现方法级缓存,通常依赖Spring Cache抽象。它并不是某个具体缓存产品的实现,而是一套统一的注解和接口规范。要启用这套规范,必须在配置类上标注@EnableCaching,而不是@EnableCacheable。不少开发者在刚开始接触时容易把两个名字记混,导致缓存配置不生效。@EnableCacheable并不是Spring官方提供的注解,如果在代码中误用,编译阶段可能报找不到符号,或者即便通过某些自定义注解绕过,运行期也不会触发缓存逻辑。下面从概念、依赖、基础用法和进阶配置几个层面展开说明。

一、先厘清注解:@EnableCaching才是开启缓存的正确姿势
Spring Cache抽象的核心入口是@EnableCaching。它通过@Import引入CachingConfigurationSelector,在Spring容器中注册缓存相关的BeanPostProcessor和拦截器。当被标注@Cacheable的方法被调用时,代理类会先检查缓存中是否已有数据,有则直接返回,没有则执行原方法并将结果写入缓存。
@EnableCacheable这个名字并不存在。如果你看某些旧教程或内部封装代码里出现类似写法,要么是拼写错误,要么是自定义了同名注解但未接入Spring Cache的拦截机制。要判断注解是否真正生效,可以观察日志中是否出现CacheAspectSupport相关代理,或者调试时方法是否被代理。
启动类上标注@EnableCaching的示例代码如下:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
二、引入依赖并配置缓存管理器
Spring Boot默认提供自动配置,只要引入spring-boot-starter-cache,就能在无任何额外配置的情况下使用基于ConcurrentHashMap的本地缓存。这种缓存适合单体应用和小数据量场景,重启即失效。对于分布式或需要持久化的场景,可以切换为Redis、Caffeine、Ehcache等。
在pom.xml中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
如果使用Redis,再添加spring-boot-starter-data-redis,并在配置文件中指定缓存类型:
spring:
cache:
type: redis
redis:
host: 127.0.0.1
port: 6379
Spring Boot会根据classpath自动创建CacheManager。默认的ConcurrentMapCacheManager不提供过期时间,数据会一直存在直到显式清除或应用重启。如果希望本地缓存支持过期策略,可以引入Caffeine并设置缓存规格。
三、@Cacheable的核心属性与基础示例
@Cacheable用于标记方法返回值需要缓存。它的常用属性包括cacheNames或value,指定缓存名称;key指定缓存键,支持SpEL表达式;condition和unless用于控制缓存行为。一个典型查询方法如下:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(cacheNames = "user", key = "#id")
public User getUserById(Long id) {
// 模拟数据库查询
System.out.println("查询数据库:" + id);
return new User(id, "用户" + id);
}
}
第一次调用getUserById(1)时,控制台会打印查询数据库,返回结果并存入名为user的缓存。第二次使用相同id调用时,方法体不会执行,直接返回缓存对象。需要注意,默认key是空字符串时,Spring会根据参数生成SimpleKey,不同参数组合可能不同,但建议显式指定key,避免混淆。
key表达式支持SpEL,例如#id表示参数id,#root.methodName表示方法名,还可以拼接字符串:#root.targetClass + '_' + #id。当参数是对象时,可以取属性:#user.id。不过表达式中不能调用任意方法,只能使用SpEL支持的运算符和属性访问。
condition和unless的区别:condition在方法执行前判断,如果为false则不会使用缓存,直接执行方法;unless在方法执行后判断,如果为true则不缓存返回结果。示例:
@Cacheable(cacheNames = "user", key = "#id",
condition = "#id != null",
unless = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id);
}
这里#result是方法返回结果,当返回null时不缓存,避免缓存穿透。
四、缓存更新与失效:@CachePut和@CacheEvict
只读缓存用@Cacheable就够了,但数据更新时如果缓存不清除,用户会拿到旧值。Spring Cache提供@CachePut和@CacheEvict。@CachePut会在方法执行后把返回值放入缓存,通常用于更新方法;@CacheEvict用于删除缓存。
@CachePut示例:
@CachePut(cacheNames = "user", key = "#user.id")
public User updateUser(User user) {
userRepository.update(user);
return user;
}
@CacheEvict示例:
@CacheEvict(cacheNames = "user", key = "#id")
public void deleteUser(Long id) {
userRepository.delete(id);
}
如果要清空整个缓存区域,可以设置allEntries = true;如果希望在方法执行前先清除缓存,可以设置beforeInvocation = true,这样即使方法抛出异常也会删除缓存。
五、自定义Key生成策略与缓存配置
当多个方法使用相同缓存名称但参数不同时,key表达式可能重复。可以全局实现KeyGenerator接口,统一生成规则。示例:
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator customKeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName());
sb.append(':').append(method.getName());
for (Object param : params) {
sb.append(':').append(param);
}
return sb.toString();
}
};
}
}
然后在@Cacheable上指定keyGenerator = "customKeyGenerator"。注意key和keyGenerator不能同时使用。
对于Redis缓存,默认使用JdkSerializationRedisSerializer,缓存对象需要实现Serializable接口。实际生产环境更推荐使用GenericJackson2JsonRedisSerializer或StringRedisSerializer,减少存储体积并支持跨语言读取。可以通过自定义CacheManager实现:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
上述代码设置了缓存过期时间为30分钟,并使用JSON序列化。这样缓存数据在Redis中可读性更强,也方便调试。
六、常见错误排查
第一个常见问题是类内部调用导致@Cacheable失效。Spring缓存依赖AOP代理,如果同一个类中的方法A直接调用方法B,方法B上的缓存注解不会生效,因为此时调用的是this对象的原始方法,没有经过代理。解决办法是将缓存方法移到独立的Service中,或者通过ApplicationContext获取代理对象再调用。
第二个常见问题是key冲突。例如两个方法都使用cacheNames = "user",且key默认使用参数id,如果参数类型相同但含义不同,可能互相覆盖。建议为每个业务方法使用独立的cacheNames,并显式定义key,例如user:info:#id、user:list:#page。
第三个常见问题是在缓存null值上处理不当。@Cacheable默认情况下如果方法返回null,Redis缓存不会存储null值,但ConcurrentMapCacheManager会存储null,需要注意。可以通过unless = "#result == null"跳过null缓存,或者配置缓存管理器允许缓存null值。一般建议主动防止缓存穿透,在查询不到数据时返回空对象或使用布隆过滤器。
最后是缓存和数据库一致性。缓存必然存在延迟,更新数据库后如果忘记清除缓存,旧数据会一直存在。建议在更新方法上同时使用@CacheEvict,或者引入消息队列通知缓存失效。对于高并发场景,还可以使用缓存双删策略,即更新前后各删除一次缓存。
Spring Boot整合缓存并不复杂,关键是理解Spring Cache抽象的设计:@EnableCaching负责总开关,@Cacheable负责读取缓存,@CachePut负责更新,@CacheEvict负责删除。实际项目中建议从默认的ConcurrentMapCacheManager入手快速验证逻辑,再根据部署架构切换到Redis等分布式缓存。配置时显式指定cacheNames和key能减少很多隐性冲突,遇到注解不生效时优先检查是否发生类内部调用。
Spring Boot缓存注解EnableCaching修改时间:2026-08-27 04:41:31