尚硅谷SpringBoot整合教程
1. JSR107
缓存开发规范,Java Caching 定义了5个核心接口, 分别是CachingProvider,CacheManager,Cache,Entry和Expiry。
-
CachingProvider:定义了创建,配置,获取,管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider.
-
CacheManager:定义了创建,配置,获取,管理和控制多个唯一命名的Cache,这些Cache存在于CacheManger的上下文中, 一个CacheManager仅被一个CachingProvider所拥有。
-
Cache:是一个类似Map的数据结构并临时存储以key为索引的值,一个Cache仅被一个CacheManger所拥有。
-
Entry: 是一个存储在Cache中的key-value对。
-
Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问,更新和删除。缓存有效期可以通过ExpiryPolicy设置。
2. Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们的开发。
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
- Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,ConcurrentMapCache等;
- 每次调用需要缓存的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果返回给用户,下次调用直接从缓存中获取;
- 使用Spring缓存抽象时我们需要关注以下两点;
- 确定方法需要被缓存以及他们的缓存策略;
- 从缓存中读取之前缓存存储的数据;
几个重要概念及缓存注解:
关键词或注解 | 解释 |
---|---|
Cache | 缓存接口,定义缓存操作;实现有:RedisCache,EncacheCache,ConcurrentMapCache等; |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件; |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 更新缓存 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
Cache SpEL Available metadata
3. 使用缓存
3.1 搭建基本环境
导入数据库文件spring_cache.sql,创建department和employee表
CREATE TABLE `department` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`department_name` varchar(300) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '部门名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
cREATE TABLE `employee` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '员工ID',
`d_id` int DEFAULT NULL COMMENT '部门ID',
`lastName` varchar(50) DEFAULT NULL COMMENT '姓名',
`gender` int DEFAULT NULL COMMENT '性别',
`email` varchar(20) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
创建SpringBoot项目 springboot01-cache ,导入相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.crysw</groupId>
<artifactId>springboot01-cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot01-cache</name>
<description>springboot01-cache</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 完善用例
创建javaBean封装数据, 因为缓存中的对象数据会序列化和反序列化,所以JavaBean需要实现序列化接口。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Department implements Serializable {
private Integer id;
private String departmentName;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee implements Serializable {
private Integer id;
private String lastName;
private String email;
/**
* 性别 1-男,0-女
*/
private Integer gender;
/**
* 部门ID
*/
private Integer dId;
}
整合myBatis操作数据库, 在全局配置文件添加相关配置。
# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cache?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
# 开启驼峰映射
mybatis.configuration.map-underscore-to-camel-case=true
# debug
debug=true
logging.level.cn.crysw.cache.mapper=debug
# redis
#spring.redis.host=192.168.111.129
编写mapper接口
@Mapper
public interface DepartmentMapper {
@Select("select id, department_name as \"departmentName\" from department where id = #{id}")
Department getDeptById(Integer id);
}
@Mapper
public interface EmployeeMapper {
@Select(value = "select * from employee where id=#{id}")
Employee getEmpById(Integer id);
@Update(value = "update employee set lastName=#{lastName}, email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}")
void updateEmp(Employee employee);
@Delete(value = "delete from employee where id=#{id}")
void deleteEmpById(Integer id);
@Insert(value = "insert into employee(lastName,email,gender, d_id) values(#{lastName},#{email},#{gender},#{dId})")
void insertEmp(Employee employee);
@Select(value = "select * from employee where lastName=#{name}")
Employee getEmpByLastName(String name);
}
3.3 缓存步骤
主启动类上开启基于注解的缓存配置 @EnableCaching
@SpringBootApplication
@MapperScan(value = "cn.crysw.cache.mapper")
@EnableCaching
public class Springboot01CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot01CacheApplication.class, args);
}
}
标注缓存注解 @Cacheable @CachePut @CacheEvict @Caching @CacheConfig;
3.3.1 注解@Cacheable
作用:将方法的运行结果进行缓存。
注解@Cacheable的几个属性介绍:
- cacheNames/value:指定缓存组件的名字
- key:缓存数据使用的key,默认是使用方法参数的值;编写SpEL: #id 参数id的值
- keyGenerator:key的生成器,可以自己指定key的生成器; key和keyGenerator二选一使用;
- cacheManager:指定缓存管理器
- cacheResolver:指定缓存解析器,cacheResolver和cacheManager二选一使用;
- condition:指定符合条件的情况下才缓存; condition = “#id>0” 表示参数id大于0时才缓存数据;
- unless: 否定缓存,当unless指定的条件为true,方法的返回值就不会缓存,可以获取到结果进行判断操作;unless = “#resultnull" 返回结果是null不缓存,unless="#a02” 表示第一个参数的值是2,结果就不缓存
- sync: 使用异步模式, 使用比较少;
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
// 没指定key或keyGenerator,系统默认按照参数为key缓存数据,后面分析原理的时候看源码
@Cacheable(cacheNames = {"emp"}/*,*/
/*key = "#root.methodName+'['+#id+']'",*/
/* keyGenerator = "myKeyGenerator",*/
/* condition = "#id>1",*/
/* unless = "#result==null", cacheManager = "employeeRedisCacheManager"*/)
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
}
通过controller调用当前service类的接口,第一调用是查询的db,第二次是从缓存中读取。
查询2号员工
2023-07-10 23:04:24.273 DEBUG 22104 --- [nio-8080-exec-4] // 查db
c.c.c.mapper.EmployeeMapper.getEmpById : ==> Preparing: select * from employee where id=?
2023-07-10 23:04:24.288 DEBUG 22104 --- [nio-8080-exec-4] c.c.c.mapper.EmployeeMapper.getEmpById : ==> Parameters: 2(Integer)
2023-07-10 23:04:24.301 DEBUG 22104 --- [nio-8080-exec-4] c.c.c.mapper.EmployeeMapper.getEmpById : <== Total: 1
2023-07-10 23:04:24.316 DEBUG 22104 --- [nio-8080-exec-4]
// 保存数据到缓存
o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@45847bd3
// 查缓存
2023-07-10 23:04:32.058 DEBUG 22104 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@45847bd3
2023-07-10 23:04:32.062 DEBUG 22104 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@45847bd3
3.3.2 注解@CachePut
作用:更新数据时更新缓存
更新员工信息的同时更新缓存, 如果没有指定key,会以返回的employee对象作为key进行缓存,会导致再次查询还是更新之前的缓存数据,因为更新的key和查询时缓存到cache中的key不一致; @CachePut 应该使用查询缓存时的key,查询使用的是参数id作为key,更新可以使用#employee.id或返回结果#result.id.
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@CachePut(cacheNames = {"emp"}, key = "#result.id", condition = "#employee.id>1")
public Employee updateEmp(Employee employee) {
System.out.println("updateEmp:" + employee);
employeeMapper.updateEmp(employee);
return employee;
}
}
3.3.3 注解@CacheEvict
作用:清除指定的缓存数据。
- key:指定要清除的数据
- allEntries=true : 删除这个缓存的所有数据
- beforeInvocation=false: 默认是在方法执行之后执行, 如果方法执行异常,缓存就不会清除;
- beforeInvocation=true,缓存的清除是否在方法执行之前执行,无论方法执行是否异常,缓存都会清除;
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@CacheEvict(cacheNames = {"emp"}, key = "#id", allEntries = true)
public void deleteEmp(Integer id) {
System.out.println("deleteEmp: " + id);
employeeMapper.deleteEmpById(id);
}
}
执行效果就是,先查询触发数据保存到缓存中; deleteEmp删除数据后再次调用查询接口,数据是从db读取而不是缓存了。
2023-07-10 23:21:34.108 INFO 14292 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' // 派发请求
2023-07-10 23:21:34.108 INFO 14292 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2023-07-10 23:21:34.118 INFO 14292 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 10 ms
2023-07-10 23:21:34.124 DEBUG 14292 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
查询2号员工 // 第一次请求getEmp查询db,然后将数据存入缓存中
2023-07-10 23:21:40.722 DEBUG 14292 --- [nio-8080-exec-1] c.c.c.mapper.EmployeeMapper.getEmpById : ==> Preparing: select * from employee where id=?
2023-07-10 23:21:40.736 DEBUG 14292 --- [nio-8080-exec-1] c.c.c.mapper.EmployeeMapper.getEmpById : ==> Parameters: 2(Integer)
2023-07-10 23:21:40.748 DEBUG 14292 --- [nio-8080-exec-1] c.c.c.mapper.EmployeeMapper.getEmpById : <== Total: 1
2023-07-10 23:21:40.784 DEBUG 14292 --- [nio-8080-exec-1]
// 数据存入缓存中
o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 第二次请求getEmp, 读取的缓存中的数据
2023-07-10 23:21:48.810 DEBUG 14292 --- [nio-8080-exec-4] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
2023-07-10 23:21:48.814 DEBUG 14292 --- [nio-8080-exec-4] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 请求delEmp,先删除缓存数据
2023-07-10 23:22:05.588 DEBUG 14292 --- [nio-8080-exec-5] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
deleteEmp: 2 // 再删除db数据
2023-07-10 23:22:05.590 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById : ==> Preparing: delete from employee where id=?
2023-07-10 23:22:05.590 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById : ==> Parameters: 2(Integer)
2023-07-10 23:22:05.597 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById : <== Updates: 1
2023-07-10 23:22:05.599 DEBUG 14292 --- [nio-8080-exec-5] o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 再次请求getEmp,先去缓存读取,没有数据
2023-07-10 23:22:14.877 DEBUG 14292 --- [nio-8080-exec-7] o.s.b.w.f.OrderedRequestContextFilter : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
查询2号员工 // 上面被删除了,缓存中没有读取到数据,重新读取db数据并缓存数据
2023-07-10 23:22:14.879 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById : ==> Preparing: select * from employee where id=?
2023-07-10 23:22:14.879 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById : ==> Parameters: 2(Integer)
2023-07-10 23:22:14.880 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById : <== Total: 0
2023-07-10 23:22:14.881 DEBUG 14292 --- [nio-8080-exec-7]
// 读取db数据后,进行数据缓存
o.s.b.w.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
3.3.4 注解@Caching
定义复杂的缓存规则, 可以同时指定缓存数据,更新缓存,删除缓存的配置。
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Caching(
cacheable = {@Cacheable(value = "emp", key = "#lastName")},
put = {@CachePut(value = "emp", key = "#result.id"),
@CachePut(value = "emp", key = "#result.email")
},
evict = {@CacheEvict(key = "#result.id")}
)
public Employee getEmpByLastName(String lastName) {
Employee emp = employeeMapper.getEmpByLastName(lastName);
System.out.println("getEmpByLastName: " + emp);
return emp;
}
}
3.3.5 注解@CacheConfig
作用:为当前类统一指定缓存特性;
- cacheNames : 指定缓存组件名称;
- keyGenerator:指定缓存组件的key生成器;
- cacheManager:指定缓存组件的缓存管理器;
- cacheResolver:指定缓存组件的缓存解析器;
@CacheConfig(cacheNames = {"emp"})
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Cacheable
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
}
4. 缓存的配置原理
4.1 CacheAutoConfiguration
SpringBoot提供了对SpringCache的自动配置支持, 相关源码在自动配置类CacheAutoConfiguration
中,里面导入了下面这些缓存配置类。
org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
动配置类CacheAutoConfiguration
的源码:
// 1
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
// 。。。省略部分代码
// 导入配置
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 获取所有支持的缓存配置类型
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
// 根据支持的缓存配置类型获取对应的缓存配置类
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
}
// 2
final class CacheConfigurations {
private static final Map<CacheType, Class<?>> MAPPINGS;
static {
Map<CacheType, Class<?>> mappings = new HashMap<CacheType, Class<?>>();
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
addGuavaMapping(mappings);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
// 容器存入所有支持的缓存配置类
MAPPINGS = Collections.unmodifiableMap(mappings);
}
@Deprecated
private static void addGuavaMapping(Map<CacheType, Class<?>> mappings) {
mappings.put(CacheType.GUAVA, GuavaCacheConfiguration.class);
}
private CacheConfigurations() {
}
// 根据缓存配置类型获取对应的缓存配置类
public static String getConfigurationClass(CacheType cacheType) {
Class<?> configurationClass = MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, "Unknown cache type " + cacheType);
return configurationClass.getName();
}
// 根据缓存配置类的名称获取缓存配置类的类型
public static CacheType getType(String configurationClassName) {
for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
if (entry.getValue().getName().equals(configurationClassName)) {
return entry.getKey();
}
}
throw new IllegalStateException(
"Unknown configuration class " + configurationClassName);
}
}
// 3
public enum CacheType {
/**
* Generic caching using 'Cache' beans from the context.
*/
GENERIC,
/**
* JCache (JSR-107) backed caching.
*/
JCACHE,
/**
* EhCache backed caching.
*/
EHCACHE,
/**
* Hazelcast backed caching.
*/
HAZELCAST,
/**
* Infinispan backed caching.
*/
INFINISPAN,
/**
* Couchbase backed caching.
*/
COUCHBASE,
/**
* Redis backed caching.
*/
REDIS,
/**
* Caffeine backed caching.
*/
CAFFEINE,
/**
* Guava backed caching.
*/
@Deprecated GUAVA,
/**
* Simple in-memory caching.
*/
SIMPLE,
/**
* No caching.
*/
NONE;
}
4.2 SimpleCacheConfiguration
系统如果没有引入其他缓存组件,默认加载的是SimpleCacheConfiguration
配置类, 开启debug的启动日志也可以看到匹配的配置类。
SimpleCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
SimpleCacheConfiguration
源码
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
// 提供了spring.cache.xxx的配置项来修改缓存相关的配置属性值
private final CacheProperties cacheProperties;
// 缓存组件定制器
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
// 配置了缓存管理器 ConcurrentMapCacheManager
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
// 如果指定了缓存组件名称, 就创建缓存组件并设置缓存组件名称
// ConcurrentMapCacheManager#setCacheNames
cacheManager.setCacheNames(cacheNames);
}
// 调用缓存组件定制器的customize方法进行定制化处理,可以自定义CacheManagerCustomizer
return this.customizerInvoker.customize(cacheManager);
}
}
4.3 CacheManager
默认使用ConcurrentMapCacheManager
缓存管理器将封装在ConcurrentMapCache的数据保存在ConcurrentMap<String, Cache> cacheMap中;开发中使用缓存中间件 redis, memcached,ehcache。
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
// 缓存容器
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
private boolean dynamic = true;
// 就设置缓存组件名称
public void setCacheNames(Collection<String> cacheNames) {
if (cacheNames != null) {
for (String name : cacheNames) {
// 创建缓存组件并存入cacheMap中
this.cacheMap.put(name, createConcurrentMapCache(name));
}
this.dynamic = false;
}
else {
this.dynamic = true;
}
}
// 如果没有设置spring.cache.cacheNames, 在第一次请求getEmp接口时也会先从读取缓存
@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
// 因为初始化时没有设置spring.cache.cacheNames, 此处获取的cache是null
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
// 系统会创建鉿缓存组件 ConcurrentMapCache
cache = createConcurrentMapCache(name);
// 将缓存组件存入cacheMap中
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
// 创建缓存组件
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
// new 缓存组件ConcurrentMapCache
return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256),
isAllowNullValues(), actualSerialization);
}
}
4.4 ConcurrentMapCache
缓存组件ConcurrentMapCache
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
// 缓存数据的ConcurrentMap容器
private final ConcurrentMap<Object, Object> store;
private final SerializationDelegate serialization;
// 创建缓存组件
protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
boolean allowNullValues, SerializationDelegate serialization) {
super(allowNullValues);
Assert.notNull(name, "Name must not be null");
Assert.notNull(store, "Store must not be null");
this.name = name;
this.store = store;
this.serialization = serialization;
}
// 先读取缓存
@Override
protected Object lookup(Object key) {
// 从缓存容器中获取指定key的值
return this.store.get(key);
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
if (this.store.containsKey(key)) {
return (T) get(key).get();
}
else {
synchronized (this.store) {
if (this.store.containsKey(key)) {
return (T) get(key).get();
}
T value;
try {
value = valueLoader.call();
}
catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
put(key, value);
return value;
}
}
}
// 缓存数据
@Override
public void put(Object key, Object value) {
// 将数据缓存到store容器中
this.store.put(key, toStoreValue(value));
}
// 删除指定的缓存
@Override
public void evict(Object key) {
this.store.remove(key);
}
// 清除所有缓存
@Override
public void clear() {
this.store.clear();
}
}
AbstractValueAdaptingCache
public abstract class AbstractValueAdaptingCache implements Cache {
// 先读取缓存
@Override
public ValueWrapper get(Object key) {
// 走子类重写 ConcurrentMapCache#lookup
Object value = lookup(key);
return toValueWrapper(value);
}
protected abstract Object lookup(Object key);
// 默认将数据封装到SimpleValueWrapper中
protected Cache.ValueWrapper toValueWrapper(Object storeValue) {
return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
}
}
public interface Cache {
String getName();
ValueWrapper get(Object key);
// 缓存数据
void put(Object key, Object value);
// 更新缓存
ValueWrapper putIfAbsent(Object key, Object value);
// 删除指定key的缓存
void evict(Object key);
// 清除所有缓存
void clear();
interface ValueWrapper {
/**
* Return the actual value in the cache.
*/
Object get();
}
}
public class SimpleValueWrapper implements ValueWrapper {
private final Object value;
/**
* Create a new SimpleValueWrapper instance for exposing the given value.
* @param value the value to expose (may be {@code null})
*/
public SimpleValueWrapper(Object value) {
this.value = value;
}
/**
* Simply returns the value as given at construction time.
*/
@Override
public Object get() {
return this.value;
}
}
4.5 debug测试
启动应用,请求getEmp接口(http://localhost:8080/emp/1
)
第一次请求接口,先按照配置的缓存名称emp查找缓存组件,第一次缓存里面肯定没有,读取结果cache是null,就会去创建缓存组件ConcurrentMapCache。
创建完缓存组件后,再次去缓存组件中根据请求参数id为key读取缓存数据, 查询结果也是null。
读取缓存结果为null后,再去执行db查询操作。
将执行db查询的结果存入缓存容器中,按照key=1进行缓存,也就是请求参数id=1的值进行的缓存,后面再介绍key为什么是请求参数id的值?
数据成功存入缓存后,再次请求 getEmp接口(http://localhost:8080/emp/1
), 首先读取缓存就有数据了。
5. KeyGenerator
查看缓存数据的key生成。
CacheAspectSupport#findCachedItem
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
// 生成key
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
// 生成key
private Object generateKey(CacheOperationContext context, Object result) {
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
"using named params on classes without debug info?) " + context.metadata.operation);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}
// 生成key
protected Object generateKey(Object result) {
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
}
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
}
生成的key是getEmp(String id)的参数id值,那这个key=2是如何生成的呢?下面SimpleKeyGenerator
然后根据key查找缓存
CacheAspectSupport#findInCaches
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
// 根据key获取缓存
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
AbstractCacheInvoker#doGet
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
try {
// 根据key获取缓存, 此处的cache.get(key)就是使用容器中默认的ConcurrentMapCache去查找缓存。
return cache.get(key);
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null; // If the exception is handled, return a cache miss
}
}
AbstractValueAdaptingCache#get 》》 ConcurrentMapCache#lookup, 从ConcurrentMap<Object, Object> store容器中查找缓存数据。
系统默认使用SimpleKeyGenerator来生成key,查看源码。
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
// 如果只有1个参数,就用当前参数作为key返回。所以上面的测试中key=id=2
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
// 如果是多个参数,就以封装参数列表的SimpleKey对象为key返回
return new SimpleKey(params);
}
}
6. 自定义KeyGenerator
配置自定义KeyGenerator, 只需要实现KeyGenerator接口,重写generate方法,返回的就是自定义的key;
public interface KeyGenerator {
/**
* Generate a key for the given method and its parameters.
* @param target the target instance
* @param method the method being called
* @param params the method parameters (with any var-args expanded)
* @return a generated key
*/
Object generate(Object target, Method method, Object... params);
}
lambda表达式风格简化了匿名内部类的实现。
/**
* 自定义KeyGenerator
*
* @Author crysw
* @Version 1.0
* @Date 2023/6/24 22:10
*/
@Configuration
public class MyCacheConfig {
@Bean(name = "myKeyGenerator")
public KeyGenerator keyGenerator() {
// 自定义key策略: 方法名 + [方法参数]
return (Object target, Method method, Object... params) -> method.getName() + "[" + Arrays.asList(params) + "]";
}
}
在缓存时,指定自定义的myKeyGenerator策略生成key进行数据缓存。
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Cacheable(cacheNames = {"emp"},
/*key= "#root.methodName+'['+#id+']'",*/
/* key = "#id",*/
keyGenerator = "myKeyGenerator",
/* condition = "#id>1"*/
unless = "#result==null")
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
}
7. 整合Redis
引入Redis的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.5.10.RELEASE</version>
</dependency>
导入Redis的自动配置依赖后,启动应用后匹配的就是RedisCacheConfiguration缓存配置注解。
RedisCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)
匹配上RedisCacheConfiguration配置,使用RedisCacheManager创建了RedisCache来作为缓存组件,RedisCache通过Redis操作缓存数据;可以类比SimpleCacheConfiguration#ConcurrentMapCacheManager
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
RedisCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
// RedisCacheManager缓存管理器
@Bean
public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
cacheManager.setUsePrefix(true);
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
// 将配置的spring.cache.cacheNames设置缓存名称
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
启动应用后,访问http://localhost:8080/emp/2
,请求的是getEmp(id=2), 发现redis Desktop Manager客户端存入的数据是乱码, 是因为Redis默认使用的是JdkSerializationRedisSerializer序列化。
RedisTemplate
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private RedisSerializer<?> defaultSerializer;
private boolean initialized = false;
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
// 如果没有设置默认的序列化器,默认使用JDK序列化器
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
initialized = true;
}
}
如果要缓存数据是我们能看懂的, 只需要自己配置其他序列化器,比如Jackson2JsonRedisSerializer
@Configuration
public class MyRedisConfig {
/**
* 自定义redisTemplate,使用Jackson2JsonRedisSerializer序列化器
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 序列化Employee对象
RedisTemplate<Object, Employee> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Employee.class));
return template;
}
@Bean
public RedisTemplate<Object, Department> departmentRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 序列化Department对象
RedisTemplate<Object, Department> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Department.class));
return template;
}
@Bean
@Primary
public RedisCacheManager employeeRedisCacheManager(RedisTemplate<Object, Employee> employeeRedisTemplate) {
// 要使用自定义的缓存管理器,因为使用了自定义的employeeRedisTemplate去操作缓存,json格式进行序列化数据
RedisCacheManager cacheManager = new RedisCacheManager(employeeRedisTemplate);
cacheManager.setUsePrefix(true);
return cacheManager;
}
@Bean
public RedisCacheManager departmentRedisCacheManager(RedisTemplate<Object, Department> departmentRedisTemplate) {
// 缓存管理器
RedisCacheManager cacheManager = new RedisCacheManager(departmentRedisTemplate);
cacheManager.setUsePrefix(true);
return cacheManager;
}
}
并且在缓存方法上指定缓存管理器cacheManager为自定义的employeeRedisCacheManager。
@Service
public class EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Cacheable(cacheNames = {"emp"},
/*key= "#root.methodName+'['+#id+']'",*/
key = "#id",
/* keyGenerator = "myKeyGenerator",*/
/* condition = "#id>1"*/
unless = "#result==null" ,cacheManager = "employeeRedisCacheManager")
public Employee getEmp(Integer id) {
System.out.println("查询" + id + "号员工");
Employee employee = employeeMapper.getEmpById(id);
return employee;
}
}
再次查看Redis客户端,以json格式缓存了emp,key=2的员工信息。