Spring 3.1新特性之三:Spring对声明式缓存的支持

一、概述:

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring Cache特点:

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的缓存方式:例如 EHCache 等集成。

特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
本文将通过实际的例子来解析Spring Cache和自定义缓存以及第三方缓存组件的区别,最后将会详细的介绍Spring Cache相关注解。
 

二、Spring对Cache的支持有两种方法

1.基于注解的配置
2.基础XML配置
 

2.1、通过注解去使用到Cache

@Cacheable:使用这个注解的方法在执行后会缓存其返回结果。
@CacheEvict:使用这个注解的方法在其执行前或执行后移除Spring Cache中的某些元素。
@CachePut:与@Cacheable不同,它虽然也可以声明一个方法支持缓存,但它执行方法前是不会去检查缓存中是否存在之前执行过的结果,而是每次都执行该方法,并将执行结果放入指定缓存中。
---------------------------------------@Cacheable----------------------------
@Cacheable可以注解在方法上也可以注解在类上。当标记在方法上面时,表示该方法是可以缓存的;如果标记在类上面,则表示该类的所有方法都是可以缓存的。对于一个支持缓存的方法,在执行后,会将其返回结果进行缓存起来,以保证下次同样参数来执行该方法的时候可以从缓存中返回结果,而不需要再次执行此方法。Spring缓存方法的返回值是以键值对进行保存的,值就是返回结果,键的话Spring支持两种策略,一种是默认策略,一种是自定义策略。
注意:一个支持缓存的方法,在对象内部被调用是不会触发缓存功能的。
@Cacheable可以指定三个属性:value、key、condition。
----------------------value:指定Cache的名称
value值是必须指定的,其表示该方法缓存的返回结果是被缓存在哪个Cache上的,对应Cache的名称。其可以是一个Cache也可以使多个Cache(数组);
key属性是用来指定Spring缓存方法返回结果时所对应的key值的。该属性支持EL表达式。当我们没有指定key时,Spring会使用默认策略生成key。·
-----------key的自定义策略:
自定义策略是表示我们通过EL表达式来指定我们的key。这里EL表达式可以使用方法参数以及他们对应的属性。使用方法参数时,我们可以使用“#参数名”。
-------1.methodName  当前方法名    #root.methodName
-------2.method       当前方法  #root.method.name
-------3.target   当前被动用对象 #root.target
-------4.targetClass      当前被调用对象Class#root.targetCla
-------5.args    当前方法参数组成的数组 #root.args[0]
-------6.caches    当前被调用方法所使用的Cache #root.caches[0],name
当我们要使用root作为key时,可以不用写root直接@Cache(key="caches[1].name")。因为他默认是使用#root的
-----------------condition:指定发生条件
有时候可能不需要缓存一个方法的所有结果。通过condition可以设置一个条件,其条件值是使用SpringEL表达式来指定的。当为true时进行缓存处理;为false时不进行缓存处理,即每次调用该方法都会执行。
-----------------------------------------------------@CachePut------------------------------------------
与@Cacheable不同,它虽然也可以声明一个方法支持缓存,但它执行方法前是不会去检查缓存中是否存在之前执行过的结果,而是每次都执行该方法,并将执行结果放入指定缓存中。
-----------------------------------------------------@CacheEvict----------------------------------------
@CacheEvict标注在需要清楚缓存元素的方法和类上。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。value表示清除缓存作用在哪个Cache上;key是表示需要清除的是哪个key。
--------------allEntries是表示是否需要清除缓存中所有的元素。
--------------beforeInvocatio
清除操作默认是在方法成功执行之后触发的。使用beforeInvocation可以改变触发清除操作的时间,当我们设置为true时,Spring会在调用该方法之前进行缓存的清除。
 
 
1.在不使用第三方缓存组件的情况下,自定义缓存实现
场景:

通过图书ID查询图书信息的方法做缓存,以图书ID为 key,图书名称为 value,当以相同的图书ID查询图书信息的时候,直接从缓存中返回结果,则不经过数据库查询,反之则查询数据库更新缓存。当然还支持 reload 缓存

具体的实体类代码如下:

  1. package com.my.data.cache.dao;
  2. import java.io.Serializable;
  3. /**
  4. * 图书领域对象
  5. * @author wbw
  6. *
  7. */
  8. public class Book implements Serializable {
  9. /**
  10. * 序列化版本号
  11. */
  12. private static final long serialVersionUID = -2710076757833997658L;
  13. /**
  14. * 图书ID
  15. */
  16. private String bookId;
  17. /**
  18. * 图书名称
  19. */
  20. private String bookName;
  21. /**
  22. * @return the 图书ID
  23. */
  24. public String getBookId() {
  25. return bookId;
  26. }
  27. /**
  28. * @param 图书ID the bookId to set
  29. */
  30. public void setBookId(String bookId) {
  31. this.bookId = bookId;
  32. }
  33. /**
  34. * @return the 图书名称
  35. */
  36. public String getBookName() {
  37. return bookName;
  38. }
  39. /**
  40. * @param 图书名称 the bookName to set
  41. */
  42. public void setBookName(String bookName) {
  43. this.bookName = bookName;
  44. }
  45. }

然后定义缓存管理器,主要用于处理新增缓存对象、删除缓存对象、更新缓存对象、查询缓存对象、清空缓存等操作

具体代码如下:

  1. package com.my.cacheManage;
  2. import java.util.concurrent.ConcurrentHashMap;
  3. /**
  4. * 自定义缓存控制器 、回调接口、监听器
  5. * 缓存代码和业务逻辑耦合度高
  6. * 不灵活
  7. * 缓存存储写的很死,不能灵活的与第三方缓存插件相结合
  8. *
  9. * @author wangbowen
  10. *
  11. * @param <T>
  12. */
  13. public class CacheManagerHandler<T> {
  14. //ConcurrentHashMap jdk1.5 线程安全 分段锁
  15. private  ConcurrentHashMap<String,T> cache = new ConcurrentHashMap<String,T>();
  16. /**
  17. * 根据key获取缓存对象
  18. * @param key 缓存对象名
  19. * @return  缓存对象
  20. */
  21. public T getValue(Object key){
  22. return cache.get(key);
  23. }
  24. /**
  25. * 新增或更新
  26. * @param key
  27. * @param value
  28. */
  29. public void  put(String key,T value){
  30. cache.put(key, value);
  31. }
  32. /**
  33. * 新增缓存对象
  34. * @param key 缓存对象名称
  35. * @param value 缓存对象
  36. * @param time 缓存时间(单位:毫秒) -1表示时间无限制
  37. * @param callBack
  38. */
  39. public void  put(String key,T value,long time,CacheCallBack callBack){
  40. cache.put(key, value);
  41. if(time!=-1){
  42. //启动监听
  43. new CacheListener(key,time,callBack);
  44. }
  45. }
  46. /**
  47. * 根据key删除缓存中的一条记录
  48. * @param key
  49. */
  50. public void evictCache(String key){
  51. if(cache.containsKey(key)){
  52. cache.remove(key);
  53. }
  54. }
  55. /**
  56. * 获取缓存大小
  57. * @return
  58. */
  59. public int getCacheSize(){
  60. return  cache.size();
  61. }
  62. /**
  63. * 清空缓存
  64. */
  65. public void evictCache(){
  66. cache.clear();
  67. }
  68. }

定义图书服务接口

  1. package com.my.data.cache.service;
  2. import com.my.data.cache.domain.Book;
  3. /**
  4. * 图书服务接口
  5. * @author wbw
  6. *
  7. */
  8. public interface BookService {
  9. /**
  10. * 根据图形ID查询图书
  11. * @param bookId 图书ID
  12. * @return 图书信息
  13. */
  14. public Book findBookById(String bookId);
  15. }

图书服务接口实现类

  1. package com.my.service.impl;
  2. import com.my.cacheManage.CacheManagerHandler;
  3. import com.my.domain.Account;
  4. import com.my.service.MyAccountService;
  5. /**
  6. * 实现类
  7. * @author wangbowen
  8. *
  9. */
  10. public class BookServiceImpl implements BookService {
  11. /**
  12. * 缓存控制器
  13. */
  14. private CacheManagerHandler<Book> myCacheManager;
  15. /**
  16. * 初始化
  17. */
  18. public BookServiceImpl(){
  19. myCacheManager = new CacheManagerHandler<Book>();
  20. }
  21. @Override
  22. public Book getBookByID(String id) {
  23. Account result = null;
  24. if(id!=null){
  25. //先查询缓存中是否有,直接返回
  26. result = myCacheManager.getValue(id);
  27. if(result!=null){
  28. System.out.println("从缓存查询到:"+id);
  29. return result;
  30. }else{
  31. result = getFormDB(id);
  32. if(result!=null){//将数据查询出来的结果更新到缓存集合中
  33. myCacheManager.put(id, result);
  34. return result;
  35. }else{
  36. System.out.println("数据库为查询到"+id+"账户信息");
  37. }
  38. }
  39. }
  40. return null;
  41. }
  42. /**
  43. * 从数据库中查询
  44. * @param name
  45. * @return
  46. */
  47. private Book getFormDB(String id) {
  48. System.out.println("从数据库中查询:"+id);
  49. return new Book(id);
  50. }
  51. }

运行执行:

  1. package com.my.cache.test;
  2. import com.my.service.MyAccountService;
  3. import com.my.service.impl.MyAccountServiceImpl;
  4. /**
  5. * 测试
  6. * @author wbw
  7. *
  8. */
  9. public class MyCacheTest {
  10. public static void main(String[] args) {
  11. BookService s = new BookServiceImpl();
  12. s.getBookByid("1");// 第一次查询,应该是数据库查询
  13. s.getBookByid("1");// 第二次查询,应该直接从缓存返回
  14. }
  15. }

控制台输出信息:

  1. 从数据库中查询:1
  2. 从缓存查询到:1

虽然自定义缓存能实现缓存的基本功能,但是这种自定义缓存存在很大的缺点:

1.缓存代码和实际业务耦合度高,不便于后期修改。

2.不灵活,需要按照某种缓存规则进行缓存,不能根据不同的条件进行缓存

3.兼容性太差,不能与第三方缓存组件兼容。

Spring Cache基于注解的实现方式:

领域对象:

 

  1. package com.my.data.cache.domain;
  2. import java.io.Serializable;
  3. import javax.persistence.Column;
  4. import javax.persistence.Entity;
  5. import javax.persistence.GeneratedValue;
  6. import javax.persistence.GenerationType;
  7. import javax.persistence.Id;
  8. import javax.persistence.Table;
  9. @Entity
  10. @Table(name="book")
  11. public class Book implements Serializable {
  12. /**
  13. *
  14. */
  15. private static final long serialVersionUID = -6283522837937163003L;
  16. @Id
  17. @GeneratedValue(strategy = GenerationType.AUTO)
  18. @Column(name = "id", nullable = true)
  19. private Integer id;
  20. private String isbn;
  21. private String title;
  22. public Book(String isbn, String title) {
  23. this.isbn = isbn;
  24. this.title = title;
  25. }
  26. public Book() {
  27. }
  28. public Book(int id, String isbn, String title) {
  29. super();
  30. this.id = id;
  31. this.isbn = isbn;
  32. this.title = title;
  33. }
  34. public int getId() {
  35. return id;
  36. }
  37. public void setId(int id) {
  38. this.id = id;
  39. }
  40. public String getIsbn() {
  41. return isbn;
  42. }
  43. public void setIsbn(String isbn) {
  44. this.isbn = isbn;
  45. }
  46. public String getTitle() {
  47. return title;
  48. }
  49. public void setTitle(String title) {
  50. this.title = title;
  51. }
  52. @Override
  53. public String toString() {
  54. return "Book{" + "isbn='" + isbn + '\'' + ", title='" + title + '\'' + '}';
  55. }
  56. }

图书服务接口

  1. package com.my.data.cache.service;
  2. import java.util.List;
  3. import com.my.data.cache.domain.Book;
  4. public interface BookService {
  5. public Book findById(Integer bid);
  6. public List<Book> findBookAll();
  7. public void insertBook(Book book);
  8. public Book findByTitle(String title);
  9. public int countBook();
  10. public void modifyBook(Book book);
  11. public Book findByIsbn(String isbn);
  12. }

图书服务接口,这里 ORM框架使用的是Spring Data 通过基于注解的查询方式能更简便的与数据交互

  1. package com.my.data.cache.service.impl;
  2. import java.util.List;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.cache.annotation.CacheEvict;
  7. import org.springframework.cache.annotation.Cacheable;
  8. import org.springframework.stereotype.Service;
  9. import org.springframework.transaction.annotation.Transactional;
  10. import com.my.data.cache.annotation.LogAnnotation;
  11. import com.my.data.cache.domain.Book;
  12. import com.my.data.cache.exception.MyException;
  13. import com.my.data.cache.repository.BookRepository;
  14. import com.my.data.cache.service.BookService;
  15. @Service
  16. @Transactional
  17. public class BookServiceImpl  implements BookService{
  18. private static final Logger log = LoggerFactory.getLogger(BookServiceImpl.class);
  19. @Autowired
  20. private BookRepository bookRepository;
  21. //将缓存保存进andCache,并使用参数中的bid加上一个字符串(这里使用方法名称)作为缓存的key
  22. @Cacheable(value="andCache",key="#bid+'findById'")
  23. @LogAnnotation(value="通过Id查询Book")
  24. public Book findById(Integer bid) {
  25. this.simulateSlowService();
  26. return bookRepository.findById(bid);
  27. }
  28. @Override
  29. public List<Book> findBookAll() {
  30. return bookRepository.findBookAll();
  31. }
  32. //将缓存保存进andCache,并当参数title的长度小于32时才保存进缓存,默认使用参数值及类型作为缓存的key
  33. @Cacheable(value="andCache",condition="#title.length >5")
  34. public Book findByTitle(String title){
  35. return null;
  36. }
  37. /**
  38. * 新增
  39. * @param book
  40. * @return
  41. */
  42. public void insertBook(Book book){
  43. bookRepository.save(book);
  44. }
  45. @Override
  46. public int countBook() {
  47. return bookRepository.countBook();
  48. }
  49. //清除掉指定key中的缓存
  50. @CacheEvict(value="andCache",key="#book.id + 'findById'")
  51. public void modifyBook(Book book) {
  52. log.info("清除指定缓存"+book.getId()+"findById");
  53. bookRepository.save(book);
  54. }
  55. //清除掉全部缓存
  56. @CacheEvict(value="andCache",allEntries=true,beforeInvocation=true)
  57. public void ReservedBook() {
  58. log.info("清除全部的缓存");
  59. }
  60. // Don't do this at home
  61. private void simulateSlowService() {
  62. try {
  63. long time = 5000L;
  64. Thread.sleep(time);
  65. } catch (InterruptedException e) {
  66. throw new MyException("程序出错", e);
  67. }
  68. }
  69. @Override
  70. public Book findByIsbn(String isbn) {
  71. return bookRepository.findByIsbn(isbn);
  72. }
  73. }

BookRepository接口

  1. package com.my.data.cache.repository;
  2. import java.util.List;
  3. import org.springframework.data.jpa.repository.Query;
  4. import org.springframework.data.repository.CrudRepository;
  5. import com.my.data.cache.dao.CommonRepository;
  6. import com.my.data.cache.domain.Book;
  7. /**
  8. * 接口
  9. * @author wbw
  10. *
  11. */
  12. public interface BookRepository extends CrudRepository<Book, Integer> {
  13. @Query("select b from Book b  where 1=1")
  14. public   List<Book> findBookAll();
  15. /**
  16. * 根据isbn查询
  17. * @param name
  18. * @return
  19. */
  20. @Query("select  b from Book b where b.id =?1")
  21. public  Book findById(Integer bid);
  22. /**
  23. * 统计size
  24. * @return
  25. */
  26. @Query("select count(*) from Book where 1=1 ")
  27. public int countBook();
  28. /**
  29. * 根据命名规范查询 findBy+属性
  30. * @param isbn
  31. * @return
  32. */
  33. public Book findByIsbn(String isbn);
  34. }

Controller 代码:

  1. package com.my.data.cache.controller;
  2. import java.util.List;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RequestMethod;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. import org.springframework.web.bind.annotation.RestController;
  11. import com.my.data.cache.domain.Book;
  12. import com.my.data.cache.service.BookService;
  13. @RestController
  14. @RequestMapping("/book")
  15. public class BookController {
  16. private static final Logger log = LoggerFactory.getLogger(BookController.class);
  17. @Autowired
  18. private BookService bookService;
  19. @RequestMapping("/{id}")
  20. public @ResponseBody Book index(@PathVariable("id") Integer id){
  21. Book b = bookService.findById(id);
  22. log.info(b.getIsbn()+"------>"+b.getTitle());
  23. return b;
  24. }
  25. @RequestMapping(value = "/list", method = RequestMethod.GET)
  26. public  @ResponseBody List<Book> list(){
  27. List<Book> b = bookService.findBookAll();
  28. return  b;
  29. }
  30. @RequestMapping(value = "/add")
  31. public String  insertBook(){
  32. Book b = new Book();
  33. b.setId(4);
  34. b.setIsbn("1111");
  35. b.setTitle("相信自己");
  36. bookService.insertBook(b);
  37. return "success";
  38. }
  39. /**
  40. * 更新
  41. * @return
  42. */
  43. @RequestMapping(value = "/update")
  44. public String update(){
  45. Book b = new Book();
  46. b.setId(1);
  47. b.setIsbn("1");
  48. b.setTitle("爱的力量");
  49. bookService.modifyBook(b);
  50. return "success";
  51. }
  52. }

测试-------这里我们采用Spring Boot 启动服务的方式,

  1. package com.my.data.cache;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.boot.CommandLineRunner;
  4. import org.springframework.boot.SpringApplication;
  5. import org.springframework.boot.autoconfigure.SpringBootApplication;
  6. import org.springframework.cache.annotation.EnableCaching;
  7. import com.my.data.cache.domain.Book;
  8. import com.my.data.cache.service.BookService;
  9. /**
  10. *
  11. * 启动器
  12. *
  13. */
  14. @SpringBootApplication
  15. @EnableCaching//扫描cahce注解
  16. public class Application1  implements CommandLineRunner{
  17. @Autowired
  18. private BookService bookService;
  19. @Override
  20. public void run(String... args) throws Exception {
  21. Book b1 = bookService.findByIsbn("1");
  22. Book b2 = bookService.findByIsbn("2");
  23. Book b3 = bookService.findById(3);
  24. System.out.println(b1);
  25. System.out.println(b2);
  26. System.out.println(b3);
  27. }
  28. public static void main(String[] args) {
  29. SpringApplication.run(Application1.class,args);
  30. }
  31. }

第一次访问indexI()方法,可以从下面的控制台信息看出:发出了sql语句从数据库查询数据,然后将查询的数据缓存,下次有相同条件访问相同的请求则直接从缓存中取数据

  1. Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=?
  2. Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=?
  3. Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.id=?
  4. Book{isbn='1', title='爱的力量'}
  5. 2016-03-10 11:22:40.107  INFO 8132 --- [  restartedMain] com.my.data.cache.Application1           : Started Application1 in 42.661 seconds (JVM running for 46.34)

第二次访问indexI()方法,则直接从缓存中获取数据,不在查询数据库

  1. Book{isbn='1', title='爱的力量'}
  2. 2016-03-10 11:27:43.936  INFO 6436 --- [  restartedMain] com.my.data.cache.Application1           : Started Application1 in 19.363 seconds (JVM running for 20.063)

从上面Spring Cahce的示例代码可以看出,Spring Cache通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果,并没有太多的缓存业务逻辑代码。
Spring Cache 部分注解介绍:

  • @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
  • @CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
  • @CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空,清除全部的缓存@CacheEvict(value="缓存名字",allEntries=true,beforeInvocation=true)
Spring Cache实现原理:
通过 Spring AOP动态代理技术
Spring Cache的扩展性:
在现实的业务中总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了。
此部分引用别人的代码示例:

首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。

利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCacheOSCache,甚至一些内存数据库例如 memcache 或者redis 等。下面我举一个简单的例子说明如何做。

  1. import java.util.Collection;
  2. import org.springframework.cache.support.AbstractCacheManager;
  3. public class MyCacheManager extends AbstractCacheManager {
  4. private Collection<? extends MyCache> caches;
  5. /**
  6. * Specify the collection of Cache instances to use for this CacheManager.
  7. */
  8. public void setCaches(Collection<? extends MyCache> caches) {
  9. this.caches = caches;
  10. }
  11. @Override
  12. protected Collection<? extends MyCache> loadCaches() {
  13. return this.caches;
  14. }
  15. }

上面的自定义的 CacheManager 实际继承了 spring 内置的 AbstractCacheManager,实际上仅仅管理 MyCache 类的实例。

下面是MyCache的定义:

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import org.springframework.cache.Cache;
  4. import org.springframework.cache.support.SimpleValueWrapper;
  5. public class MyCache implements Cache {
  6. private String name;
  7. private Map<String,Account> store = new HashMap<String,Account>();;
  8. public MyCache() {
  9. }
  10. public MyCache(String name) {
  11. this.name = name;
  12. }
  13. @Override
  14. public String getName() {
  15. return name;
  16. }
  17. public void setName(String name) {
  18. this.name = name;
  19. }
  20. @Override
  21. public Object getNativeCache() {
  22. return store;
  23. }
  24. @Override
  25. public ValueWrapper get(Object key) {
  26. ValueWrapper result = null;
  27. Account thevalue = store.get(key);
  28. if(thevalue!=null) {
  29. thevalue.setPassword("from mycache:"+name);
  30. result = new SimpleValueWrapper(thevalue);
  31. }
  32. return result;
  33. }
  34. @Override
  35. public void put(Object key, Object value) {
  36. Account thevalue = (Account)value;
  37. store.put((String)key, thevalue);
  38. }
  39. @Override
  40. public void evict(Object key) {
  41. }
  42. @Override
  43. public void clear() {
  44. }
  45. }

上面的自定义缓存只实现了很简单的逻辑,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。

这还不够,spring 还不知道我们写了这些东西,需要通过 spring*.xml 配置文件告诉它

  1. <cache:annotation-driven />
  2. <bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager">
  3. <property name="caches">
  4. <set>
  5. <bean
  6. class="com.rollenholt.spring.cache.MyCache"
  7. p:name="accountCache" />
  8. </set>
  9. </property>
  10. </bean>

测试:

  1. Account account = accountService.getAccountByName("someone");
  2. logger.info("passwd={}", account.getPassword());
  3. account = accountService.getAccountByName("someone");
  4. logger.info("passwd={}", account.getPassword());

Spring Cache的注意和限制

基于 proxy 的 spring aop 带来的内部调用问题

上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题.

如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。

  1. public Account getAccountByName2(String accountName) {
  2. return this.getAccountByName(accountName);
  3. }
  4. @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache
  5. public Account getAccountByName(String accountName) {
  6. // 方法内部实现不考虑缓存逻辑,直接实现业务
  7. return getFromDB(accountName);
  8. }

上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效

要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。

@CacheEvict 的可靠性问题

我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下

  1. // 清空 accountCache 缓存
  2. @CacheEvict(value="accountCache",allEntries=true)
  3. public void reload() {
  4. throw new RuntimeException();
  5. }

测试:

  1. accountService.getAccountByName("someone");
  2. accountService.getAccountByName("someone");
  3. try {
  4. accountService.reload();
  5. } catch (Exception e) {
  6. //...
  7. }
  8. accountService.getAccountByName("someone");

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。

那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。

上一篇:Spring Cloud Greenwich 新特性和F升级分享


下一篇:​Python 3 新特性:类型注解——类似注释吧,反正解释器又不做校验