SpringBoot缓存——基本环境搭建
SpringBoot缓存——基本环境搭建
首先选择场景启动器:
其依赖如下:
<dependencies>
<!--cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mysql-connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
项目结构:
一、使用到到的技术
Web+Mybatis+Mysql+Spring Cache
二、创建数据库和数据库表
1、新建数据库spring_cache
2、新建department表
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`departmentName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3、新建employee表
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`lastName` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`gender` int(2) DEFAULT NULL,
`d_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
三、创建实体类对象
1、Employee
public class Employee {
private Integer id;
private String lastName;
private Integer gender;
private String email;
private Integer dId;
//getter和setter方法
}
2、Department
public class Department {
private Integer id;
private String departmentName;
//getter和setter方法
}
四、整合mybatis
1、application.yml配置文件
#配置数据源
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cache?serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
#开启驼峰命名匹配规则
mybatis:
configuration:
map-underscore-to-camel-case: true
#开启日志功能(包级别),需要注意格式
logging:
level:
com.example.mapper : debug
#开启debgu调试
#debug: true
2、在启动类上加上如下注解
使用注解版的mybatis,在启动类上加上如下注解:
-
@MapperScan(value = "com.example.mapper")
:使用@MapperScan扫描mapper接口所在的包。说明:也可以在每个mapper文件上使用@Mapper注解
-
@EnableCaching
:开启基于注解的缓存功能。
@MapperScan(value = "com.example.mapper")
@SpringBootApplication
@EnableCaching
public class SpringBoot01CacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot01CacheApplication.class, args);
}
}
3、EmployeeMapper
这里使用注解版的mybatisc操作数据库。
因为在启动类使用了@MapperScan扫描mapper接口所在的包,所有这里接口上不用写@mapper注解了。
package com.example.mapper;
import com.example.bean.Employee;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
public interface EmployeeMapper {
//根据id查找emp
@Select("select * from employee where id=#{id}")
Employee getEmpById(Integer id);
//更新emp
@Update("update employee set lastName=#{lastName},email=#{email},gender=#{gender},d_id=#{dId} where id=#{id}")
void updateEmp(Employee emp);
//删除emp
@Delete("delete from employee where id=#{id}")
void deleteEmpById(Integer id);
//添加emp
@Insert("insert into employee values(default,#{lastName},#{email},#{gender},#{dId})")
void insertEmp(Employee employee);
//根据lastName查询emp
@Select("select * from employee where lastName=#{lastName}")
Employee getEmpByLastName(String lastName);
}
4、EmployeeService
几个常用的注解:
-
@Cacheable
:常用于查询方法上,将使用该注解的方法的运行结果缓存,以后再请求相同的数据时,直接从缓存中获取,不用调用方法。默认缓存的key值是该方法的参数值。如: 根据id查找:public Employee getEmpById(Integer id) {}
该方法参数id的值就是在缓存中的key的值。(不是key对应的value)
-
@CachePut
:常用于更新方法上,比如修改了数据库的某些数据,那么缓存中的数据也需要更新(如果缓存中没有该数据,那么则添加该缓存),该注解可实现修改数据并更新缓存。注意:可能会遇到以下情况:
@Cacheable(cacheNames = "emp",key = "#id")
public Employee getEmpById(Integer id) {}
@CachePut(cacheNames = "emp",key = "#emp.id") //将方法返回结果的id值作为key
public Employee updateEmp(Employee emp){return emp}
执行步骤:
-
首先根据id查询到Employee ,并将其放入缓存,缓存的参数id即为在缓存中的key。
-
更新操作,如果更新的Employee 和上面执行查询的Employee是同一条数据,那么缓存中的数据该怎么更新?
需要保证查询和更新操作之后放入缓存中数据的key是同一个。
- 以上查询方法的key默认是id和更新方法的key默认是emp,如何保证两者的key是一样的?
手动指定更新操作的key与查询操作一致。
-
-
@CacheEvict
: 缓存清除。参数:
-
cacheNames/value:指定要删除哪个缓存的数据
-
key:指定要清除的数据
-
allEntries:是否删除该cache中所有的数据,默认是false,当设置为true时,就不需要指定key了。
-
beforeInvocation:是否在该注解标注的方法执行之前清除缓存,默认是false。
-
方法调用之前和调用之后删除有什么区别?
如果方法没有出现异常,那么之前和之后清除缓存没有什么区别
但是如果方法出现了异常,那么就可以在方法执行之前清除缓存,而方法调用之后清除缓存则不能实现。
-
-
-
@CacheConfig
:可以抽取缓存的公共配置,通常使用在类上,抽取方法缓存的公共部分。
以上注解的常用属性:
cacheNames/value
:指定缓存组件的名称,将方法的返回值结果放到哪个缓存组件中,是数组的方式,可以指定多个缓存。key
:缓存数据所使用的key,默认是方法参数的值,可以使用SPEL表达式指定,如#id、root.args[0]表示的都是方法的参数。keyGenerator
:key的生成器,可以自己声明key生成器的组件id,与key二选一。cacheManager
:指定缓存管理器。cacheResolver
:指定获取解析器,cacheManager与cacheResolver二选一使用。condition
:指定符合条件的情况下使用缓存,如:condition="#id>0",参数id的值大于0的情况下加入缓存。unless
:除非,否定缓存,当unless条件成立,为true时,不会加入缓存。与condition相反。可以获取到结果进行判断,如unless="#result==null"的话就不缓存。
package com.example.service;
import com.example.bean.Employee;
import com.example.mapper.EmployeeMapper;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
// @CacheConfig(cacheNames = "emp") //抽取缓存的公共配置
public class EmployeeService {
@Resource
private EmployeeMapper employeeMapper;
/**
* @Cacheable 该注解将方法的运行结果缓存,以后再要相同的数据,从缓存中获取,不用调用方法。
*
* CacheManager管理多个Cache组件,对缓存的CRUD操作是在Cache组件中,每一个缓存组件都有一个唯一的名称。
* 属性:
* cacheNames/value:指定缓存组件的名称,将方法的返回值结果放到哪个缓存组件中,是数组的方式,可以指定多个缓存。
* key:缓存数据使用的key,默认是方法参数的值,可以使用SPEL表达式指定,如#id、root.args[0]表示的都是方法的参数。
* 例如:key="#root.MethodName+'['+#id+']'",得到的key就是getEmp[id]的形式。
* keyGenerator:key的生成器,可以自己声明key生成器的组件id,与key二选一。
* 自定义步骤:在配置类中注册一个KeyGenerator类型的组件,然后自定义生成的内容返回。如:keyGenerator = "myKeyGenerator"
* cacheManager:指定缓存管理器,
* cacheResolver:指定获取解析器,cacheManager与cacheResolver二选一使用。
* condition:指定符合条件的情况下使用缓存,如:condition="#id>0",参数id的值大于0的情况下加入缓存。
* 多个条件之间可以使用and连接,如:condition="#id>0 and #root.methodName eq 'aaa'"
* unless:除非,否定缓存,当unless条件成立,为true时,不会加入缓存。与condition相反。
* 可以获取到结果进行判断,如unless="#result==null"的话就不缓存。
* sync:是否使用异步模式
*
* 原理:
* 1.缓存的自动配置类:CacheAutoConfiguration
* 2.缓存的配置类:
* org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
* org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
* 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.SimpleCacheConfiguration
* org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
* 3.哪个缓存配置默认生效,在主配置类中开启debug=true
* 结果是:SimpleCacheConfiguration
* 这个缓存配置类做了什么工作?有什么作用?
* 给容器中注册了一个ConcurrentMapCacheManager缓存管理器。作用是可以获取和创建ConcurrentMapCache类型的
* 缓存组件,从而将数据保存在ConcurrentMap中。
* 4.发送查询请求:localhost:8080/getEmpById/2
* 运行流程:
* @Cacheable
* 1.在方法运行之前,先去查询Cache(缓存组件),按照指定的cacheNames指定的名字获取;(cacheManager先获取指定的缓存)
* 如果第一次获取缓存没有cache组件会自动创建。
* 2.去cache中查找缓存的内容,使用一个key,默认就是方法的参数。
* key是按照某种策略生成的,默认使用的是KeyGenerator生成的,默认是使用SimpleKeyGenerator生成key.
* SimpleKeyGenerator生成key的默认策略:
* 如果没有参数:key=new SimpleKey()
* 如果有一个参数:key=参数的值
* 如果有多个参数:key=new SimpleKey(params)
* 3.没有查询到缓存就调用目标方法
* 4.将目标方法返回的结果放入到缓存。
*
* In short,@Cacheable标注的方法执行之前先检查缓存中有没有这个数据,默认是按照参数的值作为key去查询缓存,
* 如果缓存中没有就执行目标方法并将返回结果放入到缓存,以后再来调用就可以使用缓存中的数据。
*
* 核心:
* 1)使用CacheManager【ConcurrentMapCacheManager】按照cacheNames得到cache【ConcurrentMapCache】组件
* 2)key是使用keyGenerator生成的,默认是SimpleKeyGenerator
*
* @param id
* @return
*/
@Cacheable(cacheNames = "emp",key = "#id")
//根据id查找emp
public Employee getEmpById(Integer id) {
System.out.println("service查询了"+id+"号员工");
return employeeMapper.getEmpById(id);
}
/**
* @CachePut 既调用方法,又缓存数据。同步更新缓存。
* 修改了数据库的某个数据,同时更新缓存。
* 运行时机:
* 1.先调用目标方法
* 2.将目标方法的结果缓存起来
* 测试步骤:
* 1.查询1号员工,查询的结果会放入缓存中
* key:1 value:zhansgan
* 2.再次查询还是之前的结果
* 3.更新1号员工
* key:传入的employee对象 value:返回的employee对象
* 4.查询1号员工?结果还是更新之前的结果。
* 解决方法:
* key = "#emp.id"
* 或key="#result.id"
* 将key设置成与之前参数一样的值。
*
*/
@CachePut(cacheNames = "emp",key = "#emp.id")
//更新emp
public Employee updateEmp(Employee emp) {
System.out.println("service更新了"+emp.getId()+"号员工");
employeeMapper.updateEmp(emp);
return emp;
}
/**
* @CacheEvict 缓存清除
* 参数:
* cacheNames/value:指定要删除哪个缓存的数据
* key:指定要清除的数据
* allEntries:是否删除该cache中所有的数据,默认是false,当设置为true时,就不需要指定key了。
* beforeInvocation:是否在该注解标注的方法执行之前清除缓存,默认是false。
* 方法调用之前和调用之后删除有什么区别?
* 如果方法没有出现异常,那么之前和之后清除缓存没有什么区别
* 但是如果方法出现了异常,那么就可以在方法执行之前清除缓存,而方法调用之后清除缓存则不能实现。
*
*
*/
// @CacheEvict(cacheNames = "emp",allEntries = true,beforeInvocation = true)
// @CacheEvict(cacheNames = "emp",key = "#id")
// @CacheEvict(cacheNames = "emp",allEntries = true)
@CacheEvict(cacheNames = "emp",allEntries = true,beforeInvocation = true)
//删除emp
public void deleteEmpById(Integer id) {
employeeMapper.deleteEmpById(id);
//这里模拟了除数为0的异常
// int i = 10 / 0;
}
//添加emp
public void insertEmp(Employee employee) {
employeeMapper.insertEmp(employee);
}
//定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(cacheNames = "emp",key = "#lastName")
},
put = {
@CachePut(cacheNames = "emp",key = "#result.id"),
@CachePut(cacheNames = "emp",key = "#result.email")
}
)
//根据lastName查询emp
public Employee getEmpByLastName(String lastName) {
System.out.println("service getEmpByLastName查询了lastName为:"+lastName+"的员工");
return employeeMapper.getEmpByLastName(lastName);
}
}
5、EmployeeController
package com.example.controller;
import com.example.bean.Employee;
import com.example.service.EmployeeService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @author: nie
* @create: 2020-10-03 11:21
* @description:
**/
@RestController
public class EmployeeController {
@Resource
private EmployeeService employeeService;
@GetMapping("/getEmpById/{id}")
public Employee getEmpById(@PathVariable("id") Integer id) {
return employeeService.getEmpById(id);
}
@PutMapping("/updateEmp")
public String updateEmp(Employee employee) {
employeeService.updateEmp(employee);
return "success";
}
@DeleteMapping("/deleteEmpById/{id}")
public String deleteEmpById(@PathVariable("id") Integer id) {
employeeService.deleteEmpById(id);
return "success";
}
@PostMapping("/insertEmp")
public String insertEmp(Employee employee) {
employeeService.insertEmp(employee);
return "success";
}
//根据lastName查询emp
@GetMapping("/getEmpByLastName/{lastName}")
public Employee getEmpByLastName(@PathVariable("lastName") String lastName) {
return employeeService.getEmpByLastName(lastName);
}
}
6、自定义cache中key的生成策略
package com.example.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author: nie
* @create: 2020-10-03 17:08
* @description: 自定义cache中key的生成策略
**/
@Configuration //标记这是一个配置类
public class MyKeyGeneratorConfig {
@Bean("myKeyGenerator") //将该组件加入到容器中
//注意导的包是:org.springframework.cache.interceptor.KeyGenerator
public KeyGenerator keyGenerator() {
//使用匿名内部类的方式
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
System.out.println(o);
return method.getName() + "[" + Arrays.asList(objects).toString() + "]";
}
};
}
}