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 缓存

具体的实体类代码如下:

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

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

具体代码如下:

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

定义图书服务接口

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

图书服务接口实现类

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

运行执行:

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

控制台输出信息:

[html] view plain copy
 
  1. 从数据库中查询:1  
  2. 从缓存查询到:1  

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

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

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

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

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

领域对象:

 

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

图书服务接口

[java] view plain copy
 
  1. package com.my.data.cache.service;  
  2.   
  3. import java.util.List;  
  4.   
  5. import com.my.data.cache.domain.Book;  
  6.   
  7. public interface BookService {  
  8.   
  9.     public Book findById(Integer bid);  
  10.   
  11.     public List<Book> findBookAll();  
  12.   
  13.     public void insertBook(Book book);  
  14.   
  15.     public Book findByTitle(String title);  
  16.       
  17.     public int countBook();  
  18.       
  19.     public void modifyBook(Book book);  
  20.       
  21.      public Book findByIsbn(String isbn);  
  22.       
  23.       
  24. }  

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

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

BookRepository接口

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

Controller 代码:

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


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

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

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

[html] view plain copy
 
  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()方法,则直接从缓存中获取数据,不在查询数据库

[java] view plain copy
 
  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 等。下面我举一个简单的例子说明如何做。

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

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

下面是MyCache的定义:

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

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

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

[html] view plain copy
 
  1. <cache:annotation-driven />   
  2.   
  3.  <bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager">  
  4.      <property name="caches">   
  5.        <set>   
  6.          <bean   
  7.            class="com.rollenholt.spring.cache.MyCache"  
  8.            p:name="accountCache" />   
  9.        </set>   
  10.      </property>   
  11.    </bean>   
测试:
[html] view plain copy
 
  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 都会失效,我们来演示一下。


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

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

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

@CacheEvict 的可靠性问题

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

[java] view plain copy
 
  1. // 清空 accountCache 缓存  
  2.  @CacheEvict(value="accountCache",allEntries=true)  
  3.  public void reload() {   
  4.    throw new RuntimeException();   
  5.  }  
  6.    
测试:
[java] view plain copy
 
  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,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。

原文地址:https://www.cnblogs.com/duanxz/p/4893219.html