概述:
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 和自定义缓存管理者,具有相当的灵活性和扩展性
具体的实体类代码如下:
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";
}
}
测试-------这里我们采用Spring Boot 启动服务的方式,
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,即能够达到缓存方法的返回对象的效果,并没有太多的缓存业务逻辑代码。Spring Cache 部分注解介绍:
- @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
- @CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
- @CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空,清除全部的缓存@CacheEvict(value="缓存名字",allEntries=true,beforeInvocation=true)
首先,我们需要提供一个 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 的 spring aop 带来的内部调用问题
上面介绍过 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 的可靠性问题
我们看到,@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,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。