概述:
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring Cache特点:
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的缓存方式:例如 EHCache 等集成。
特点总结如下:
具体的实体类代码如下:
package com.my.data.cache.dao; import java.io.Serializable; /** * 图书领域对象 * @author wbw * */ public class Book implements Serializable { /** * 序列化版本号 */ private static final long serialVersionUID = -2710076757833997658L; /** * 图书ID */ private String bookId; /** * 图书名称 */ private String bookName; /** * @return the 图书ID */ public String getBookId() { return bookId; } /** * @param 图书ID the bookId to set */ public void setBookId(String bookId) { this.bookId = bookId; } /** * @return the 图书名称 */ public String getBookName() { return bookName; } /** * @param 图书名称 the bookName to set */ public void setBookName(String bookName) { this.bookName = bookName; } }然后定义缓存管理器,主要用于处理新增缓存对象、删除缓存对象、更新缓存对象、查询缓存对象、清空缓存等操作
具体代码如下:
package com.my.cacheManage; import java.util.concurrent.ConcurrentHashMap; /** * 自定义缓存控制器 、回调接口、监听器 * 缓存代码和业务逻辑耦合度高 * 不灵活 * 缓存存储写的很死,不能灵活的与第三方缓存插件相结合 * * @author wangbowen * * @param <T> */ public class CacheManagerHandler<T> { //ConcurrentHashMap jdk1.5 线程安全 分段锁 private ConcurrentHashMap<String,T> cache = new ConcurrentHashMap<String,T>(); /** * 根据key获取缓存对象 * @param key 缓存对象名 * @return 缓存对象 */ public T getValue(Object key){ return cache.get(key); } /** * 新增或更新 * @param key * @param value */ public void put(String key,T value){ cache.put(key, value); } /** * 新增缓存对象 * @param key 缓存对象名称 * @param value 缓存对象 * @param time 缓存时间(单位:毫秒) -1表示时间无限制 * @param callBack */ public void put(String key,T value,long time,CacheCallBack callBack){ cache.put(key, value); if(time!=-1){ //启动监听 new CacheListener(key,time,callBack); } } /** * 根据key删除缓存中的一条记录 * @param key */ public void evictCache(String key){ if(cache.containsKey(key)){ cache.remove(key); } } /** * 获取缓存大小 * @return */ public int getCacheSize(){ return cache.size(); } /** * 清空缓存 */ public void evictCache(){ cache.clear(); } }定义图书服务接口
package com.my.data.cache.service; import com.my.data.cache.domain.Book; /** * 图书服务接口 * @author wbw * */ public interface BookService { /** * 根据图形ID查询图书 * @param bookId 图书ID * @return 图书信息 */ public Book findBookById(String bookId); }图书服务接口实现类
package com.my.service.impl; import com.my.cacheManage.CacheManagerHandler; import com.my.domain.Account; import com.my.service.MyAccountService; /** * 实现类 * @author wangbowen * */ public class BookServiceImpl implements BookService { /** * 缓存控制器 */ private CacheManagerHandler<Book> myCacheManager; /** * 初始化 */ public BookServiceImpl(){ myCacheManager = new CacheManagerHandler<Book>(); } @Override public Book getBookByID(String id) { Account result = null; if(id!=null){ //先查询缓存中是否有,直接返回 result = myCacheManager.getValue(id); if(result!=null){ System.out.println("从缓存查询到:"+id); return result; }else{ result = getFormDB(id); if(result!=null){//将数据查询出来的结果更新到缓存集合中 myCacheManager.put(id, result); return result; }else{ System.out.println("数据库为查询到"+id+"账户信息"); } } } return null; } /** * 从数据库中查询 * @param name * @return */ private Book getFormDB(String id) { System.out.println("从数据库中查询:"+id); return new Book(id); } }运行执行:
package com.my.cache.test; import com.my.service.MyAccountService; import com.my.service.impl.MyAccountServiceImpl; /** * 测试 * @author wbw * */ public class MyCacheTest { public static void main(String[] args) { BookService s = new BookServiceImpl(); s.getBookByid("1");// 第一次查询,应该是数据库查询 s.getBookByid("1");// 第二次查询,应该直接从缓存返回 } }控制台输出信息:
从数据库中查询:1 从缓存查询到:1虽然自定义缓存能实现缓存的基本功能,但是这种自定义缓存存在很大的缺点:
1.缓存代码和实际业务耦合度高,不便于后期修改。
2.不灵活,需要按照某种缓存规则进行缓存,不能根据不同的条件进行缓存
3.兼容性太差,不能与第三方缓存组件兼容。
Spring Cache基于注解的实现方式:
领域对象:
package com.my.data.cache.domain; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="book") public class Book implements Serializable { /** * */ private static final long serialVersionUID = -6283522837937163003L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = true) private Integer id; private String isbn; private String title; public Book(String isbn, String title) { this.isbn = isbn; this.title = title; } public Book() { } public Book(int id, String isbn, String title) { super(); this.id = id; this.isbn = isbn; this.title = title; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } @Override public String toString() { return "Book{" + "isbn='" + isbn + '\'' + ", title='" + title + '\'' + '}'; } }图书服务接口
package com.my.data.cache.service; import java.util.List; import com.my.data.cache.domain.Book; public interface BookService { public Book findById(Integer bid); public List<Book> findBookAll(); public void insertBook(Book book); public Book findByTitle(String title); public int countBook(); public void modifyBook(Book book); public Book findByIsbn(String isbn); }图书服务接口,这里 ORM框架使用的是Spring Data 通过基于注解的查询方式能更简便的与数据交互
package com.my.data.cache.service.impl; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.my.data.cache.annotation.LogAnnotation; import com.my.data.cache.domain.Book; import com.my.data.cache.exception.MyException; import com.my.data.cache.repository.BookRepository; import com.my.data.cache.service.BookService; @Service @Transactional public class BookServiceImpl implements BookService{ private static final Logger log = LoggerFactory.getLogger(BookServiceImpl.class); @Autowired private BookRepository bookRepository; //将缓存保存进andCache,并使用参数中的bid加上一个字符串(这里使用方法名称)作为缓存的key @Cacheable(value="andCache",key="#bid+'findById'") @LogAnnotation(value="通过Id查询Book") public Book findById(Integer bid) { this.simulateSlowService(); return bookRepository.findById(bid); } @Override public List<Book> findBookAll() { return bookRepository.findBookAll(); } //将缓存保存进andCache,并当参数title的长度小于32时才保存进缓存,默认使用参数值及类型作为缓存的key @Cacheable(value="andCache",condition="#title.length >5") public Book findByTitle(String title){ return null; } /** * 新增 * @param book * @return */ public void insertBook(Book book){ bookRepository.save(book); } @Override public int countBook() { return bookRepository.countBook(); } //清除掉指定key中的缓存 @CacheEvict(value="andCache",key="#book.id + 'findById'") public void modifyBook(Book book) { log.info("清除指定缓存"+book.getId()+"findById"); bookRepository.save(book); } //清除掉全部缓存 @CacheEvict(value="andCache",allEntries=true,beforeInvocation=true) public void ReservedBook() { log.info("清除全部的缓存"); } // Don't do this at home private void simulateSlowService() { try { long time = 5000L; Thread.sleep(time); } catch (InterruptedException e) { throw new MyException("程序出错", e); } } @Override public Book findByIsbn(String isbn) { return bookRepository.findByIsbn(isbn); } }BookRepository接口
package com.my.data.cache.repository; import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import com.my.data.cache.dao.CommonRepository; import com.my.data.cache.domain.Book; /** * 接口 * @author wbw * */ public interface BookRepository extends CrudRepository<Book, Integer> { @Query("select b from Book b where 1=1") public List<Book> findBookAll(); /** * 根据isbn查询 * @param name * @return */ @Query("select b from Book b where b.id =?1") public Book findById(Integer bid); /** * 统计size * @return */ @Query("select count(*) from Book where 1=1 ") public int countBook(); /** * 根据命名规范查询 findBy+属性 * @param isbn * @return */ public Book findByIsbn(String isbn); }
Controller 代码:
package com.my.data.cache.controller; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import com.my.data.cache.domain.Book; import com.my.data.cache.service.BookService; @RestController @RequestMapping("/book") public class BookController { private static final Logger log = LoggerFactory.getLogger(BookController.class); @Autowired private BookService bookService; @RequestMapping("/{id}") public @ResponseBody Book index(@PathVariable("id") Integer id){ Book b = bookService.findById(id); log.info(b.getIsbn()+"------>"+b.getTitle()); return b; } @RequestMapping(value = "/list", method = RequestMethod.GET) public @ResponseBody List<Book> list(){ List<Book> b = bookService.findBookAll(); return b; } @RequestMapping(value = "/add") public String insertBook(){ Book b = new Book(); b.setId(4); b.setIsbn("1111"); b.setTitle("相信自己"); bookService.insertBook(b); return "success"; } /** * 更新 * @return */ @RequestMapping(value = "/update") public String update(){ Book b = new Book(); b.setId(1); b.setIsbn("1"); b.setTitle("爱的力量"); bookService.modifyBook(b); return "success"; } }
package com.my.data.cache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import com.my.data.cache.domain.Book; import com.my.data.cache.service.BookService; /** * * 启动器 * */ @SpringBootApplication @EnableCaching//扫描cahce注解 public class Application1 implements CommandLineRunner{ @Autowired private BookService bookService; @Override public void run(String... args) throws Exception { Book b1 = bookService.findByIsbn("1"); Book b2 = bookService.findByIsbn("2"); Book b3 = bookService.findById(3); System.out.println(b1); System.out.println(b2); System.out.println(b3); } public static void main(String[] args) { SpringApplication.run(Application1.class,args); } }第一次访问indexI()方法,可以从下面的控制台信息看出:发出了sql语句从数据库查询数据,然后将查询的数据缓存,下次有相同条件访问相同的请求则直接从缓存中取数据
Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=? Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=? Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.id=? Book{isbn='1', title='爱的力量'} 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()方法,则直接从缓存中获取数据,不在查询数据库
Book{isbn='1', title='爱的力量'} 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,即能够达到缓存方法的返回对象的效果,并没有太多的缓存业务逻辑代码。
首先,我们需要提供一个 CacheManager
接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。
利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCache
、OSCache
,甚至一些内存数据库例如 memcache
或者redis
等。下面我举一个简单的例子说明如何做。
import java.util.Collection; import org.springframework.cache.support.AbstractCacheManager; public class MyCacheManager extends AbstractCacheManager { private Collection<? extends MyCache> caches; /** * Specify the collection of Cache instances to use for this CacheManager. */ public void setCaches(Collection<? extends MyCache> caches) { this.caches = caches; } @Override protected Collection<? extends MyCache> loadCaches() { return this.caches; } }
上面的自定义的 CacheManager 实际继承了 spring 内置的 AbstractCacheManager,实际上仅仅管理 MyCache 类的实例。
下面是MyCache的定义:
import java.util.HashMap; import java.util.Map; import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; public class MyCache implements Cache { private String name; private Map<String,Account> store = new HashMap<String,Account>();; public MyCache() { } public MyCache(String name) { this.name = name; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public Object getNativeCache() { return store; } @Override public ValueWrapper get(Object key) { ValueWrapper result = null; Account thevalue = store.get(key); if(thevalue!=null) { thevalue.setPassword("from mycache:"+name); result = new SimpleValueWrapper(thevalue); } return result; } @Override public void put(Object key, Object value) { Account thevalue = (Account)value; store.put((String)key, thevalue); } @Override public void evict(Object key) { } @Override public void clear() { } }
上面的自定义缓存只实现了很简单的逻辑,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。
这还不够,spring 还不知道我们写了这些东西,需要通过 spring*.xml 配置文件告诉它
<cache:annotation-driven /> <bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager"> <property name="caches"> <set> <bean class="com.rollenholt.spring.cache.MyCache" p:name="accountCache" /> </set> </property> </bean>测试:
Account account = accountService.getAccountByName("someone"); logger.info("passwd={}", account.getPassword()); account = accountService.getAccountByName("someone"); logger.info("passwd={}", account.getPassword());
上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题.
如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。
public Account getAccountByName2(String accountName) { return this.getAccountByName(accountName); } @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String accountName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(accountName); }
上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效
要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。
我们看到,@CacheEvict
注释有一个属性 beforeInvocation
,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下
// 清空 accountCache 缓存 @CacheEvict(value="accountCache",allEntries=true) public void reload() { throw new RuntimeException(); }测试:
accountService.getAccountByName("someone"); accountService.getAccountByName("someone"); try { accountService.reload(); } catch (Exception e) { //... } accountService.getAccountByName("someone");
注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。
那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。