(以下内容参照自官方文档;p4jorm下载地址http://blog.csdn.net/partner4java/article/details/8629578;cache demo下载地址http://download.csdn.net/detail/partner4java/5102477)
若您只想尽快简单的使用,可以直接跳转到“第三部分:Hello World”
1、引言:
从3.1开始,spring框架提供了非侵入式的缓存支持。类似事务支持(transaction)一样简单,且通过定义注解来提供统一进行缓存支持。
2、理解缓存:
在互联网行业,一般使用的是MySQL数据库,不会像oracle一样包含了强大的缓存策略,所以往往大并发访问瓶颈在数据库访问上。
减少数据库访问,一般信息界面可以采用静态化,但是缓存无疑会是一个简单高效的方式(一般会采用OScache -- spring mvc进行了单独支持)。在统一的页面缓存背后还有一些数据是很少变化的,所以仅仅依靠页面缓存也是会照成不必要的“资源”浪费,设置一层数据缓存也是很重要的(借助ehcache或memcached,memcached互联网公司采用的比较多)。
(需要注意的是:无论何种缓存方案都会存在数据实效性问题。且,spring的cache方案还需要相同参数调用同一个方法在数据一致的情况下返回结果也应该是一致的。)
使用spring cache只需要完成两部分:
·缓存声明:在方法上加上相应缓存注解和相应策略
·configuration:定义缓存位置和具体保存策略
(spring cache并不是完全由spring提供,和transaction一样,只是对第三方框架进行上层封装)
第一分部:缓存声明
3、基于注解的缓存声明:
我们只需要学习四个注解:@Cacheable、@CachePut、 @CacheEvict 和@Caching。
@Cacheable annotation:
正如其名字,@Cacheable用于添加在需高速缓存的方法上。这些方法默认会以参数为主键把返回结果存储到高速缓存中,以便在随后的调用(使用相同的参数)方法,直接返回高速缓存中的值,不需要实际执行此方法。
最简单的方式,只需要声明一个相关缓存策略的名称:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
也可以设置多个缓冲块,其中一个缓冲块命中即会返回,并会同步其他缓存块:
@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}
默认缓存主键:
缓存是采用键值的方式存储,所以每次调用都要将相应的参数转化成一个合适的高效缓存主键。
默认的主键生成策略:
·如果没有参数,返回0;
·如果存在一个参数,则返回该参数实例;
·如果不止一个参数,返回所有参数的哈希计算值。
也可以同时实现org.springframework.cache.KeyGenerator来定义自己特定的主键生成策略。
自定义缓存主键:
由于缓存块是通用的,所以不能简单的进行缓存主键声明,这样将导致生成的主键与业务不服或者与其他业务重复,如:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
看上去应该并不是所有参数对于生成缓存主键都是有意义的。
像这种情况下,允许通过key属性来指定主键生成策略,且key支持使用SpEL:
@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);
@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);
@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);
上面的key可以简单的通过SpEL调用参数的属性。
缓存条件(何时缓存):
同样可以对condition参数通过SpEL表达式,当返回true时将被缓存,若返回false会执行方法。
如下,当名字长度小于32时才会缓存:
@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)
SpEL使用上下文:
@CachePut annotation:
用法类似于@Cacheable,但是用于存放数据声明(如更新数据),所以每次都会执行,将执行后的结果存入缓存。
所以不建议把@CachePut and @Cacheable放在同一方法上,对于需要更新的数据我们应使用@CachePut。
@CacheEvict annotation:
此注解对于去除无效的数据是非常重要的。@CacheEvict用于触发去除缓存中的数据。
除了和上面的注解用于标识缓存策略、主键和判断方式等外,又添加了allEntries属性,用于标识是否不仅只删除基于主键的数据:
@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch);
如上当需要清除一个“区域”的所有数据,而不是只清除一条数据,所以即使指定主键也是无用的。
初次之外还提供了一个beforeInvocation属性,用于表示是在执行前清除还是之后。这个属性是很有用的,比如当你在声明一个更新缓存方法之上(结合@Cacheable的场景)。
If the method does not execute (as it might be cached) or an exception is thrown, the eviction does not occur.
The latter (beforeInvocation=true) causes the eviction to occur always, before the method is invoked - this is useful in cases where the eviction does not need to be tied to the method outcome.
但是当无返回值(void)时,结合Cacheable将没有什么意义。
It is important to note that void methods can be used with @CacheEvict - as the methods act as triggers, the return values are ignored (as they don't interact with the cache) - this is not the case with @Cacheable which adds/update data into the cache and thus requires a result.
@Caching annotation:
有时我们需要添加多个注解,可以通过此注解嵌套在一起。
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key =
"#p0") })
public Book importBooks(String deposit, Date date);
自定义注解:
有时缓存的一些注解配置是常用重复的,为了避免来回拷贝,你可以自定义自己的注解,如:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value=“books”, key="#isbn")
public @interface SlowService {
}
然后你就可以通过你自定义的@SlowService注解替代下面的做法:
@Cacheable(value="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
替代方式:
@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
(这样Spring就会自动识别,当然前提是你已经开启了缓存注解支持)
学习了,注解如何使用,那么如何通知Spring扫描相关注解呢?
缓存注解开启开关:
有时,你需要一个统一的开关进行控制缓存的开启和关闭。
只需要在你标注了@Configuration注解的类上添加@EnableCaching注解即可。
@Configuration
@EnableCaching
public class AppConfig {
}
或者通过XML方式配置使用的缓存:annotation-driven
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http:// www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http:// www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/ cache/spring-cache.xsd"> <cache:annotation-driven /> </beans>
类似于@Transactional这两种方式通过AOP进行实现。
Cache annotation settings:
XML Attribute | Annotation Attribute | Default |Description
cache-manager N/A (See CachingConfigurer Javadoc) cacheManager Name of cache manager to use. Only required if the name of the cache manager is not cacheManager, as in the example above.
mode mode proxy The default mode "proxy" processes annotated beans to be proxied using Spring's AOP framework (following proxy semantics, as discussed above, applying to method calls coming in through the proxy only).
The alternative mode "aspectj" instead weaves the affected classes with Spring's AspectJ caching aspect, modifying the target class byte code to apply to any kind of method call.
AspectJ weaving requires springaspects. jar in the classpath as well as load-time weaving (or compile-time weaving) enabled.
proxy-targetclass proxyTargetClass false Applies to proxy mode only.
Controls what type of caching proxies are created for classes annotated with the @Cacheable or @CacheEvict annotations.
If the proxy-targetclass attribute is set to true, then class-based proxies are created.
If proxy-targetclass is false or if the attribute is omitted, then standard JDK interface-based proxies are created.
order order Ordered.LOWEST_PRECEDENCE Defines the order of the cache advice that is applied to beans annotated with @Cacheable or @CacheEvict.
(For more information about the rules related to ordering of AOP advice, see the section called “Advice ordering”.)
No specified ordering means that the AOP subsystem determines the order of the advice.
注意<cache:annotation-driven/>仅对上下文程序有效,如你添加到了WebApplicationContext那么仅对Controller有效,对你的service是无效的。这一点你没必要太纠结,若对IoC了解不是很透彻。
如果你使用的JDK代理方式,你只能在public类型方法上使用,若你想添加在私有方法上,需切换到AspectJ模式。
Spring建议将缓存注解标注在实现类而非接口上,因为标注在接口上且使用了类代理而非接口代理,当运行其他AOP反射时,将无法得知,因为注解是不会被继承的。
<cache:annotation-driven />用于指定了缓存“封装”策略,那么具体的“实现”策略如何指定呢?
第二部分:configuration
4、Configuring the cache storage:
使用过Spring的同学应该很清楚,通过IoC,spring让各个层面的实现易于替换,spring cache也是如此。
添加缓存注解声明后,我们需要指定一个缓存管理器--“明确”具体的数据保存策略。
下面我们列出了两种:通过ConcurrentMap和ehcache。
JDK ConcurrentMap-based Cache:
<!-- generic cache manager --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/> </set> </property> </bean>
上面的cache策略,简单的通过SimpleCacheManager来实现,可以用来测试或简单缓存(但是会非常高效,这种简单方式并没有指定超时等策略),配置中添加了两个缓存名称default、books。
Ehcache-based Cache:
ehcache的实现放在包org.springframework.cache.ehcache中,同样,也只需要非常简单的配置:
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cachemanager- ref="ehcache"/> <!-- Ehcache library setup --> <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:configlocation=" ehcache.xml"/>
首先通过factory指定了ehcache配置文件位置并创建CacheManager,然后赋予EhCacheCacheManager。
声明“空”缓存:
有时你需要测试无缓存的环境,且不想修改注解或其他配置,可参照:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"> <property name="cacheManagers"> <list> <ref bean="jdkCache"/> <ref bean="gemfireCache"/> </list> </property> <property name="fallbackToNoOpCache" value="true"/> </bean>list中存放实际缓存策略,fallbackToNoOpCache声明无缓存。
除了以上两种缓存策略,你也可以自定义所需的策略,思路是通过CacheManager、Cache两个类的封装,org.springframework.cache.support下的类可以帮助实现。
第一部分扩充:基于XML的缓存
如果项目不允许或者不习惯使用注解,也可以像transaction一样,使用XML方式进行声明指定。
cache:advice配置中添加了对bookService的缓存策略,缓存策略名为books,对方法findBook进行了缓存,主键为#isbn。还包括清除缓存的声明。
aop:advisor通过AspectJ的方式进行了通知。
方法和transaction非常相似,可参考相关文章。
通过XML方式有一些好处:对已有代码没有任何侵入;便于统一指定范围内的类等
第三部分:Hello World
(首先本demo采用:spring3.1.2.RELEASE + struts2.3.8 +hibernate4.1.10.Final;访问类似地址http://localhost:8080/cache/;CURD操作采用了本博主的P4jORM框架,请参照相关文章;先跑起来项目添加一些数据熟悉一下工程)
Demo1:在“spring_cache_demo没添加cache之前原始项目.zip”基础之上,通过简单的三步,来实现第一个cache功能 -- 缓存根据id获取数据方法
第一步:添加缓存注解
// 添加缓存声明,demo1为缓存策略名称(我们会在XML中声明此策略) @Cacheable("demo1") @Override public User get(Serializable entityId) { return super.get(entityId); }第二步:打开缓存注解(我们采用的XML开启方式)
<cache:annotation-driven />
第三步:声明具体缓存策略
<cache:annotation-driven cache-manager="cacheManager" /> <!-- generic cache manager --> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default" /> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="demo1" /> </set> </property> </bean>还对action的更新动作进行了修改(这一步和Demo实现步骤无关,只是为了更直接的显示缓存效果):
/** * 更新动作 * * @return */ public String edit() { User user = userService.get(form.getUserId()); // 又定义一个新的user是因为在测试缓存时,避免缓存策略是采用指针直接引用 User mUser = new User(); BeanUtils.copyProperties(user, mUser); mUser.setUserName(form.getUserName()); userService.update(mUser); return viewList(); }现在我们对一条数据进行修改,点击修改,提交,查看数据库的数据是已经更改生效的,但是再次点击该数据更改界面还是显示更改前的名称。充分说明了@Cacheable后不再从数据库中获取数据,而是从缓存中获取。
@Cacheable的具体使用规则请参考文章中相关解释。
Demo2:更新数据时,使缓存失效,下一次从数据库中获取
在Demo1基础上,更改如下即可:
@CacheEvict(value = "demo1", key = "#entity.userId") @Override public void update(Object entity) { super.update(entity); }@CacheEvict的使用规则参照文章中相关解释。
试验步骤:打开列表页 -- > 修改数据--提交-- >刷新列表页数据已被更改-- >继续点击此数据修改连接(关闭只打开不修改),显示的数据为修改后的,证明@CacheEvict发挥了清除缓存的功能-- >然后直接SQL通过工具修改数据库该字段(一定要在修改后再次点击修改之后)-- > 再次点击列表页中此数据的修改连接,回显的数据非数据库实时数据,而是上次修改后的值,证明获取继续走了缓存。
Demo3:将更新后的数据进行缓存,且更新前清除缓存
//不添加@CacheEvict,只添加@Cacheable,更新是无效的 @CacheEvict(value = "demo1", key = "#entity.userId", beforeInvocation = true) @Cacheable(value = "demo1", key = "#entity.userId") @Override public User updateUser(User entity) { // 更改了原有设计,返回了更改后的对象 super.update(entity); return entity; }测试步骤:打开列表页 --> 修改一条数据 --> 打开数据库,验证更新生效,且从数据库中修改本条数据 --> 继续修改此条数据,回显数据为界面更改后的,不是数据库中实时数据(因为更新的主键和查询缓存主键相同)
也可以通过一个注解实现:
@CachePut(value = "demo1", key = "#entity.userId") @Override public User updateUser(User entity) { // 更改了原有设计,返回了更改后的对象 super.update(entity); return entity; }
Demo4:自定义缓存主键生成策略
@Cacheable("demo1") @Override public PageData<User> query(Object formbean, PageIndex pageIndex, LinkedHashMap<String, OrderType> orderby) { return super.query(formbean, pageIndex, orderby); }默认缓存方式,会以参数的hash值为key:
public class DefaultKeyGenerator implements KeyGenerator { public static final int NO_PARAM_KEY = 0; public static final int NULL_PARAM_KEY = 53; public Object generate(Object target, Method method, Object... params) { if (params.length == 1) { return (params[0] == null ? NULL_PARAM_KEY : params[0]); } if (params.length == 0) { return NO_PARAM_KEY; } int hashCode = 17; for (Object object : params) { hashCode = 31 * hashCode + (object == null ? NULL_PARAM_KEY : object.hashCode()); } return Integer.valueOf(hashCode); } }透过默认提供的key生成方式可以看到,若默认key策略,参数对象需要尽量重写hashCode(),保证相同数据生成的hashCode值相同,否则会出现问题(相同查询参数,但生成的缓存key不同,进而导致无法缓存,且使缓存爆满)。
自定义key生成只需要简单两步:
第一步:自定义主键策略
public class MyKeyGenerator extends DefaultKeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { // Object keyGenerator = super.generate(target, method, params); StringBuffer buffer = new StringBuffer(); Class entityClass = GenericsHelper.getSuperGenericsClass(target.getClass()); buffer.append(entityClass.getName()); if (params != null && params.length > 1) { for (Object obj : params) { if (obj != null) { if (obj instanceof AtomicInteger || obj instanceof AtomicLong || obj instanceof BigDecimal || obj instanceof BigInteger || obj instanceof Byte || obj instanceof Double || obj instanceof Float || obj instanceof Integer || obj instanceof Long || obj instanceof Short) { buffer.append(obj); } else if (obj instanceof List || obj instanceof Set || obj instanceof Map) { buffer.append(obj); } else { buffer.append(obj.hashCode()); } } } } System.out.println("key-buffer:" + buffer.toString()); int keyGenerator = buffer.toString().hashCode(); return keyGenerator; } }第二步:配置策略
<cache:annotation-driven cache-manager="cacheManager" key-generator="keyGenerator" /> <!-- 自定义cache主键生成策略 --> <bean id="keyGenerator" class="com.partner4java.helper.MyKeyGenerator" />
Demo5:ehcache支持
我们接下来是介绍的Spring如何借助ehcache来对bean(dao、service、controller...)的调用结果进行缓存。(一般还有另外一种结合方案,如hibernate本身支持对ehcache的结合)
缓存注解无需变更和SimpleCacheManager一致,没有任何区别,所用其他缓存策略,只需要更改配置即可:
首先配置文件分两部分,spring指定ehcache和ehcache本身的缓存策略配置:
<cache:annotation-driven cache-manager="cacheManager" key-generator="keyGenerator" /> <!-- spring-cache:cache相关 --> <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:configLocation="classpath:META-INF/ehcache.xml" /> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cacheManager-ref="cacheManagerFactory" />ehcache.xml:
<?xml version="1.0" encoding="UTF-8"?> <!-- • timeToIdleSeconds – The maximum number of seconds an element can exist in the cache without being accessed. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTI eviction takes place (infinite lifetime). • timeToLiveSeconds – The maximum number of seconds an element can exist in the cache regardless of use. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTL eviction takes place (infinite lifetime). • maxElementsOnDisk – The maximum sum total number of elements (cache entries) allowed for a distributed cache in all Terracotta clients. If this target is exceeded, eviction occurs to bring the count within the allowed target. The default value is 0, which means no eviction takes place (infinite size is allowed). Note that this value reflects storage allocated on the Terracotta Server Array. A setting of 0 means that no eviction of the cache's entries takes place on Terracotta Server Array, and consequently can cause the servers to run out of disk space. • eternal – If the cache–s eternal flag is set, it overrides any finite TTI/TTL values that have been set. --> <ehcache> <defaultCache maxElementsInMemory="10" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false"> </defaultCache> <cache name="demo1" maxElementsInMemory="20000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="1800" overflowToDisk="false" /> <!-- <terracottaConfig url="localhost:9510"/> --> </ehcache>加入相关的jar:
<!-- 不能使用2.5:否则会报错误“1. Use one of the CacheManager.create() static” --> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.4.7</version> </dependency>