Spring Cache 是 Spring Framework 提供的一个强大的缓存抽象层,它允许开发者通过简单的注解方式将缓存功能集成到应用程序中,而无需关心底层缓存实现的细节。下面我将从多个方面详细介绍 Spring Cache。
Spring Cache 基于以下几个核心概念构建:
缓存抽象接口:Spring 定义了一套缓存抽象接口,如 Cache
和 CacheManager
,使应用能够与各种缓存实现进行交互。
声明式缓存:通过注解(如 @Cacheable
、@CachePut
、@CacheEvict
等)来声明缓存行为,无需编写复杂的缓存逻辑代码。
缓存管理器:CacheManager
接口负责创建、配置和管理缓存实例。
缓存解析器:CacheResolver
接口用于确定在特定操作中应该使用哪些缓存。
键生成器:KeyGenerator
接口用于生成缓存键,默认实现使用方法参数。
Spring Cache 提供了几个核心注解:
@Cacheable:标记方法的返回值应该被缓存。如果缓存中已存在相同的键,则直接返回缓存值,否则执行方法并缓存结果。
@CachePut:强制执行方法并将结果放入缓存,不考虑缓存中是否已存在相同的键。
@CacheEvict:标记方法用于从缓存中移除数据。
@Caching:组合多个缓存注解。
@CacheConfig:在类级别设置缓存配置。
Spring Cache 支持多种缓存管理器实现:
下面是一个使用 Spring Boot 和 Redis 配置 Spring Cache 的示例:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity> getUser(@PathVariable Long id) {
Optional user = userService.getUserById(id);
return user.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity saveUser(@RequestBody User user) {
return ResponseEntity.ok(userService.saveUser(user));
}
@DeleteMapping("/{id}")
public ResponseEntity deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/cache")
public ResponseEntity clearCache() {
userService.clearAllUserCache();
return ResponseEntity.noContent().build();
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 缓存注解示例
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public Optional getUserById(Long id) {
return userRepository.findById(id);
}
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 清空所有用户缓存
@CacheEvict(value = "users", allEntries = true)
public void clearAllUserCache() {
// 方法体为空,仅用于触发缓存清除
}
}
@Repository
public class UserRepository {
private final Map userMap = new HashMap<>();
// 初始化一些测试数据
public UserRepository() {
userMap.put(1L, new User(1L, "张三", 25));
userMap.put(2L, new User(2L, "李四", 30));
userMap.put(3L, new User(3L, "王五", 35));
}
public Optional findById(Long id) {
System.out.println("从数据库查询用户: " + id);
return Optional.ofNullable(userMap.get(id));
}
public User save(User user) {
userMap.put(user.getId(), user);
System.out.println("保存用户到数据库: " + user);
return user;
}
public void deleteById(Long id) {
userMap.remove(id);
System.out.println("从数据库删除用户: " + id);
}
}
public class User implements Serializable {
private Long id;
private String name;
private Integer age;
// 构造函数、getter和setter方法
public User() {}
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
// getter和setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + '}';
}
}
@SpringBootApplication
@EnableCaching // 启用缓存功能
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
package com.example.demo;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testGetUserById_CacheHit() {
// 第一次调用 - 应该从数据库获取
Optional user1 = userService.getUserById(1L);
assertTrue(user1.isPresent());
assertEquals("张三", user1.get().getName());
// 第二次调用 - 应该从缓存获取
Optional user2 = userService.getUserById(1L);
assertTrue(user2.isPresent());
assertEquals("张三", user2.get().getName());
// 验证两次获取的是同一个对象(缓存命中)
assertSame(user1.get(), user2.get());
}
@Test
void testSaveUser() {
// 创建新用户
User newUser = new User(4L, "赵六", 40);
// 保存用户,应该触发缓存更新
User savedUser = userService.saveUser(newUser);
assertEquals("赵六", savedUser.getName());
// 获取用户,应该从缓存中获取
Optional retrievedUser = userService.getUserById(4L);
assertTrue(retrievedUser.isPresent());
assertEquals("赵六", retrievedUser.get().getName());
}
@Test
void testDeleteUser() {
// 确保用户存在
Optional user = userService.getUserById(1L);
assertTrue(user.isPresent());
// 删除用户,应该触发缓存清除
userService.deleteUser(1L);
// 再次获取用户,应该从数据库获取且不存在
Optional deletedUser = userService.getUserById(1L);
assertFalse(deletedUser.isPresent());
}
}
这个注解标记方法的返回值应该被缓存。常用属性:
value
/cacheNames
:指定缓存名称。key
:指定缓存键,可以使用 SpEL 表达式。condition
:指定缓存条件,使用 SpEL 表达式。unless
:指定不缓存的条件,使用 SpEL 表达式。sync
:是否同步缓存,防止缓存击穿。这个注解标记方法的返回值应该被更新到缓存中,无论缓存中是否已存在该键。常用属性与@Cacheable
相同。
这个注解标记方法用于从缓存中移除数据。常用属性:
value
/cacheNames
:指定缓存名称。key
:指定要移除的缓存键。allEntries
:是否移除所有缓存项。beforeInvocation
:是否在方法执行前移除缓存。这个注解用于组合多个缓存注解。
这个注解用于类级别,设置缓存的默认配置。
可以通过实现KeyGenerator
接口来自定义缓存键的生成策略:
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component("myKeyGenerator")
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 自定义键生成逻辑
return target.getClass().getSimpleName() + "_" + method.getName() + "_" + String.join("_", params);
}
}
可以通过实现CacheResolver
接口来自定义缓存解析策略。
使用condition
和unless
属性可以实现条件缓存:
@Cacheable(value = "users", key = "#id", condition = "#id > 0", unless = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
使用sync
属性可以防止缓存击穿:
@Cacheable(value = "users", key = "#id", sync = true)
public User getUserById(Long id) {
// 当缓存不存在时,只有一个线程会执行此方法
return userRepository.findById(id).orElse(null);
}