1. 背景
上一篇我们使用编程式缓存管理方式,演示了缓存如何配置,如何手工编程使用。些微的有难么一丝丝麻烦,所以本篇及其简洁的声明式缓存管理来了,直接奉上,简单粗暴,体会Spring之美。
当然,与声明式事务管理(使用注解开启事务)一样,使用注解的声明式缓存管理,也是通过AOP实现的,这个之前也论述的很清楚了,通过Spring AOP封装模板代码,是Spring里面非常惯用的封装技巧。
2. 总体流程
OK,首先也是建立一个Spring的工程,引入相关的jar包,注意除了一直在用的Spring相关包,还需要一个aspectjweaver-1.8.1.jar。
然后通过配置类,配置CacheManager,我们需要使用的缓存名称。CacheManager缓存管理器是Spring提供好的缓存模块,拿来直接用就是了,它本质上是一个字典容器。
最后,对需要使用缓存的方法,直接添加注解即可开启注解,Spring Cache会自动根据缓存名称+参数是否相同来决定真实调用方法还是直接返回缓存。
由于演示实例中只对博客表进行访问,所以只需要一个缓存,名称定义为blogs即可。
3. 具体实例
3.1 编写SpringConfig配置类
启用缓存,启用自动扫描,注册数据源dataSource,注册数据库操作组件namedParameterJdbcTemplate,然后注册缓存管理器cacheManage,都是常规操作了,不多扯了,代码奉上:
@Configuration // 配置类
@ComponentScan(basePackages = { "org.maoge.cachedemo.byannotation" }) // 扫描包以便发现注解配置的bean
@EnableCaching // 启用缓存
public class SpringConfig {
// 配置数据源
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/myblog?useUnicode=true&characterEncoding=utf-8");
dataSource.setUsername("root");
dataSource.setPassword("Easy@0122");
return dataSource;
}
// 配置namedParameterJdbcTemplate组件
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
NamedParameterJdbcTemplate template = new NamedParameterJdbcTemplate(dataSource());// 注入dataSource
return template;
}
// 配置缓存管理器
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
// 缓存管理器中有很多缓存caches,其中一个名字为blogs
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("blogs")));
return cacheManager;
}
}
3.2 编写数据DO、数据访问DAO
编写数据BlogDo,与数据库表一一对应
public class BlogDo {
private Long id;
private String title;
private String author;
private String content;
//省略get set...
}
编写数据访问BlogDao,用于操作数据库,此时重点来了,增删改查那是样样不缺啊,完美!
@Repository // 注册为bean
public class BlogDao {
@Autowired // 自动注入
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
/**
* 按id查询
*/
public BlogDo getById(Long id) {
System.out.println("执行getById");
Map<String, Object> map = new HashMap<>();
map.put("id", id);
return namedParameterJdbcTemplate.queryForObject("select * from blog where id=:id", map,
new RowMapper<BlogDo>() {
@Override
public BlogDo mapRow(ResultSet rs, int rowNum) throws SQLException {
BlogDo blog = new BlogDo();
blog.setAuthor(rs.getString("author"));
blog.setContent(rs.getString("content"));
blog.setId(rs.getLong("id"));
blog.setTitle(rs.getString("title"));
return blog;
}
});
}
/**
* 查询列表
*/
@Cacheable("blogs")
public List<BlogDo> getList() {
System.out.println("执行getList");
return namedParameterJdbcTemplate.query("select * from blog", new RowMapper<BlogDo>() {
@Override
public BlogDo mapRow(ResultSet rs, int rowNum) throws SQLException {
BlogDo blog = new BlogDo();
blog.setAuthor(rs.getString("author"));
blog.setContent(rs.getString("content"));
blog.setId(rs.getLong("id"));
blog.setTitle(rs.getString("title"));
return blog;
}
});
}
/**
* 新增
*/
public void insert(BlogDo blog) {
System.out.println("执行insert");
Map<String, Object> map = new HashMap<>();
map.put("author", blog.getAuthor());
map.put("content", blog.getContent());
map.put("title", blog.getTitle());
// 注意使用:xxx占位
namedParameterJdbcTemplate.update("insert into blog(author,content,title)values(:author,:content,:title)", map);
}
/**
* 删除
*/
public void delete(Long id) {
System.out.println("执行delete");
Map<String, Object> map = new HashMap<>();
map.put("id", id);
namedParameterJdbcTemplate.update("delete from blog where id =:id", map);
}
/**
* 更新
*/
public void update(BlogDo blog) {
System.out.println("执行update");
Map<String, Object> map = new HashMap<>();
map.put("author", blog.getAuthor());
map.put("content", blog.getContent());
map.put("title", blog.getTitle());
map.put("id", blog.getId());
namedParameterJdbcTemplate.update("update blog set author=:author,content=:content,title=:title where id=:id",
map);
}
}
3.3 编写数据服务Service,并开启缓存
通过为方法添加注解@Cacheable("blogs"),表示该方法如果存在缓存,则直接取缓存,不必真正执行方法。
同时通过为方法添加注解@CacheEvict(value="blogs",allEntries=true),来清空名称为blogs的所有缓存条目。
具体实现如下:
@Service // 注册bean
public class BlogService {
@Autowired // 自动注入
private BlogDao blogDao;
// 缓存,当入参出现相同值时会直接返回缓存值
@Cacheable("blogs")
public BlogDo getById(Long id) {
return blogDao.getById(id);
}
// 缓存,没有参数方法,再次调用该方法会直接返回缓存值
@Cacheable("blogs")
public List<BlogDo> getList() {
return blogDao.getList();
}
// 执行该方法会清空blogs缓存的所有条目
@CacheEvict(value = "blogs", allEntries = true)
public void add(BlogDo blog) {
blogDao.insert(blog);
}
// 执行该方法会清空blogs缓存的所有条目
@CacheEvict(value = "blogs", allEntries = true)
public void remove(Long id) {
blogDao.delete(id);
}
// 执行该方法会清空blogs缓存的所有条目
@CacheEvict(value = "blogs", allEntries = true)
public void edit(BlogDo blog) {
blogDao.update(blog);
}
}
3.4 编写测试类,具体解释缓存执行过程
最后,我们通过构造测试类,来看看缓存到底是如何生效的。
public class Main {
public static void main(String[] args) throws SQLException {
// 获取容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
// 获取blogService组件
BlogService blogService = context.getBean("blogService", BlogService.class);
// 第一组测试
blogService.getById(1L);//控制台输出:执行getById,第一次调用无缓存,确实调用了BlogDao的getById方法
blogService.getById(1L);//控制台无输出,已经存在针对参数1L的缓存,所以此时不会调用BlogDao.getById
// 第二组测试
blogService.getById(2L);//控制台输出:执行getById,此时没有参数为1L的缓存,所以调用
blogService.getById(2L);//控制台无输出,已存在缓存
// 第三组测试
blogService.getList();//控制台输出:执行getList,此时无对应缓存
blogService.getList();//控制台无输出,已有缓存
// 第三组测试
blogService.add(new BlogDo());//注意add方法上添加了@CacheEvict(value = "blogs", allEntries = true),所以执行该方法会清空blogs相关的缓存条目
blogService.getById(1L);//控制台输出:执行getById,因为缓存已经被清空了
blogService.getById(2L);//控制台输出:执行getById,因为缓存已经被清空了
blogService.getList();//控制台输出:执行getList,因为缓存已经被清空了
}
}
可见,当我们执行新增、修改、删除等操作时,直接将blogs相关的缓存条目全部清除,所以下次再调用查询肯定会真正查询数据库。
但是当我们第二次执行一个查询方法,且参数相同,此时缓存已存在,所以不再真正访问数据库了,直接返回缓存。
4. 补充
上面的做法,比较简单粗暴,如果发生了更新,直接强制清空所有缓存,如果要管理的更精细一点的话,直接使用@CacheEvict,会仅仅删除指定参数对应的缓存,同时如果直接使用@CachePut,会仅仅将方法的返回值加入缓存。
这样会更加减少对数据库的访问次数,但是控制粒度这么细,要设计的精细点,别出现什么问题导致数据发生变化了还一直在取缓存。
OK,我认为缓存应该是对小数据量的(相对于关系数据库中海量的存储)、高访问频次的、低修改频次的热点数据的操作,既然修改频次低且数据量小,所以完全可以采用当发生变化则直接清空所有缓存条目的做法,虽然简单粗暴,但是毕竟安全可靠~