java中利用spring cache解耦业务中的缓存

虽然以前实现缓存的方式,是定义了缓存操作接口,可以灵活实现不同的缓存,可毕竟精力有限,要完成不同的缓存实现也是件麻烦的事。更要命的是,业务代码中有大量缓存操作的代码,耦合度太高,看着很不优雅。
所以呢,抽空了解了一下其它实现方案。这不,spring3.1开始,支持基于注解的缓存,算是目前我比较可以接受的一种方案吧。学完之后还是做一下笔记吧。
spring cache是一套基于注解实现的缓存技术,其本身是并不是具体实现,不过默认实现了ConcurrentMap和EHCache实现的缓存。当然也是支持其它缓存的。
spring cache有哪些特性:
1.通过少量的配置 annotation 注解即可使得既有代码支持缓存 (非常节省开发时间)
2.支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存(位于spring-context包中,spring web项目都会引用这个包)
3.支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition (支持SpEL语法)
4.支持 AspectJ,并通过其实现任何方法的缓存支持 (默认基于AOP方案,采用AspectJ会更灵活,下文有介绍)
5.支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性(如果SpEL达不到你的预期,勇敢实现自己的KeyGenerator吧)
6.支持各种缓存实现,默认是基于ConcurrentMap实现的ConcurrentMapCache,同时支持ehcache实现。若要使用redis等缓存,引入redis的实现包即可。
有哪些遗憾呢?在我考虑的场景下,还有下面的一些遗憾:
1.不支持TTL,也就是不能设置expires time。这点挺遗憾的,spring-cache认为这是各个cache实现自己去完成的事情,有方案但是只能设置统一的过期时间,这明显不够灵活。比如用户的抽奖次数、有效期等业务,当天有效,或者3天、一周有效,我们倾向于设置缓存有效期解决这个问题,而spring-cache却无法完成。
2.无法根据查询结果中的内容生成缓存key,比如getUser(uid)方法,想通过查询出来的user.email生成缓存key就无法实现了。
3.调试起来麻烦
先贴一段以前的代码,看看以前是怎么做的:
 /**
     * 先取cache。如果没有,从DB取,再存cache
     */
    @Override
    public UserDetail getUserDetail(int userId) {
        Result<String> info = cacheManager.get(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userId));
        if (StringUtils.isNotEmpty(info.getEntity())) {
            UserDetail userDetail = JSONUtils.fromJSON(info.getEntity(), UserDetail.class);
            if (null != userDetail) {
                return userDetail;
            }
        }
        UserDetail userDetail = userDetailDAO.getUserDetail(userId);
        if (userDetail != null) {
            if (logger.isDebugEnabled()) {
                logger.info("getUserDetail from db userDetail=" + userDetail.toString());
            }
            addToCache(userDetail);
            return userDetail;
        }
        return null;
    }
     private void addToCache(UserDetail userDetail) {
        cacheManager.put(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userDetail.getId()),
                JSONUtils.toJSON(userDetail), UCConstants.USER_DETAIL_CACHE_TIME);
        //节约空间,存id吧。多查一次吧
        cacheManager.put(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_NAME_KEY, userDetail.getUserNickName()),
                userDetail.getId() + "", UCConstants.USER_DETAIL_CACHE_TIME);
    }
那采用注解驱动的spring-cache之后代码怎么写的:
@Cacheable(key="#uid", value = "userCache")
     public UserDetail getUserDetail(int uid){
           return userDetailMapper.getUser(uid);
     }
当然,以前的代码写了两个缓存key,这点通过@Caching注解也是可以实现的,示例如下:
@Caching
     (evict={@CacheEvict(value="userCache",key="#user.uid"),
                @CacheEvict(value="userCache",key="#user.email")})
看了以上的代码,觉得用注解驱动的cache是不是很过瘾?代码简介,低耦合。简直是广大程序猿的福音。要像上面那样写代码,其实也挺容易的。下面就从头开始吧。
来,先把配置弄起。spring这点就很不爽,什么都要弄个配置。不过也正是基于配置+注解的方式,使得我们脱离了不断去new对象的苦海。
我的Cache是不打算使用ConcurrentMapCache的,所以我就直接拿EHCache来做示例好了。在使用EHCache之前,我们需要引入ehcache的包,以及配置ehcache.xml文件。
当然,在线上生产环境中,还是不建议只用ehcache这种方式。可以采用ehcache+redis,或者redis的方案都可以。
Maven的pom.xml文件配置:
<dependency>
     <groupId>net.sf.ehcache</groupId>
     <artifactId>ehcache-core</artifactId>
     <version>2.6.9</version>
</dependency>
ehcache的配置,位于classpath下的ehcache.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
    <diskStore path="D:/cache" /> <!-- 缓存存放目录(此目录为放入系统默认缓存目录),也可以是”D:/cache“ java.io.tmpdir -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
    <cache name="userCache"
           maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
</ehcache>
这里说明一下,为什么两个cache呢,用一个不好吗?
我最早的时候是一个defaultCache,没有userCache。后来发现spring-cache配置的EhCacheCacheManager总是load失败,报错误:
loadCaches must not return an empty Collection
我很纳闷,这不是有一个defaultCache吗?于是我跟踪代码,在AbstractCacheManager源码的afterPropertiesSet方法中有如下代码:
public void afterPropertiesSet() {
           Collection<? extends Cache> caches = loadCaches();
           Assert.notEmpty(caches, "loadCaches must not return an empty Collection");
           this.cacheMap.clear();
           // preserve the initial order of the cache names
           for (Cache cache : caches) {
                this.cacheMap.put(cache.getName(), cache);
                this.cacheNames.add(cache.getName());
           }
     }
这里取到的caches居然为empty!确实我也不知道为什么会这样。我当时尝试增加了一个名为default的cache配置,结果ehcache又报错,提示已经有名为default的cache了。于是将name改为userCache,问题解决。
从ehcache的报错来看,ehcache应该配置了一个名为default的cache,但不知道为什么spring-cache认不到。知道的同学可以告诉下我。
配置好ehcache后,就该配置我们的spring-cache了,为了方便管理,我倾向于配置独立的spring-cache.xml文件放在spring的配置目录下。内容如下:
<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"
     xmlns:p="http://www.springframework.org/schema/p"
     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 cache-manager="ehCacheManager"/>
     <!--  缓存  属性-->
    <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation"  value="classpath:ehcache.xml"/>
    </bean>
     <!-- generic cache manager -->
     <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
         <property name="cacheManager"  ref="ehCacheManagerFactory"/>
     </bean>
</beans>
当然,如果你想在没有缓存的环境中不做任何代码上的修改(比如环境迁移、临时测试等),即可简单的切换,那也是OK的。
又或者,你的环境中既有ehcache,又有redis,还有ConcurrentMapCache,那也是可以的。
上面说到的亮点,你可以用CompositeCacheManager完成。
需要重新配置以下的cacheManager:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
        <list>
            <ref bean="ehCacheManager"/>
            <ref bean="otherCachaManager"/>
        </list>
</property>
    <property name="fallbackToNoOpCache" value="true"/>
</bean>
fallbackToNoOpCache参数决定了在没有Cachhe的情况下会出现什么现象。如果为true,则会直接忽略掉缓存,可能进入db查询;如果为false(默认为false),则在无缓存时会抛出异常:
Cannot find cache named [userCache] for CacheableOperation

 

配置好了,那么该干正事了。spring-cache的使用非常简单,只会用简单的几个注解即可。那么,有哪些注解呢?看看下表:

 

Spring Cache配置

JSR-107规范

描述

@Cacheable

@CacheResult

缓存方法返回的结果,有三个参数,分别是value(缓存名称)、key(缓存key)、condition(缓存条件)

@CachePut

@CachePut

缓存方法返回的结果,并且会在方法被调用的时候执行。参数同Cacheable

@CacheEvict

@CacheRemove

清除缓存。有五个参数,除过上面的3个外,还有2个:allEntries(是否清除所有缓存)、beforeInvocation(是否在方法调用前就清除,默认为false,因此当方法抛出异常则缓存不会被清掉)

@CacheEvict(allEntries=true)

@CacheRemoveAll

清除所有缓存

@CacheConfig

@CacheDefaults

在类级别上提供一些公共配置,比如value值,每个方法都一样,就只需要在class上配置一次就好了,这个属性很有用,可惜spring3.1不支持。

 

简单解释一下,上面的表示官方文档中的内容。左边是spring3.1关于Cache操作的注解;中间是JSR-107规范的注解,spring在4.1版本实现;右边是解释。我就懒得翻译了。

 

各个注解的作用与配置方法也有作者写的不错,我就直接拿来用了:

 


 

@Cacheable、@CachePut、@CacheEvict 注释介绍

通过上面的例子,我们可以看到 spring cache 主要使用两个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict,我们总结一下其作用和配置方法。

表 1. @Cacheable 作用和配置方法

@Cacheable  的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@Cacheable 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

表 2. @CachePut 作用和配置方法

@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
@CachePut 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)

表 3. @CacheEvict 作用和配置方法

 

@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
@CacheEvict 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@CachEvict(value=”mycache”) 或者
@CachEvict(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@CachEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 例如:
@CachEvict(value=”testcache”,
condition=”#userName.length()>2”)
allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如:
@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如:

@CachEvict(value=”testcache”,beforeInvocation=true)

 


 

关于上面的注解作用与含义,没有多少再补充解释的了,原作者的文章介绍的也很详细。这里补充一下spring-cache的cache配置。cache元素配置除过cache-manager属性外,还有许多属性,简单罗列如下:

 

XML属性

注解属性

默认值

含义

cache-manager

cacheManager

默认的cacheManager名称。一个默认的CacheResolver在cacheManager的后台被初始化,要想更精细的管理缓存可以考虑设置cache-resolver属性

cache-resolver

SimpleCacheResolver

CacheResolver的bean名称,这个非必需属性,仅仅作为cache-manager属性的替代

key-generator

SimpleKeyGenerator

自定义的 key generator

error-handler

SimpleCacheErrorHandler

自定义的Cache Error handler,默认情况下,异常会直接抛出给客户端

mode

mode

proxy

spring cache默认使用了spring的AOP框架来通过proxy的方式处理注解,另一种可代替的方式就是aspectj。通过Spring AOP方式的cache注解,无法在内部调用的时候被proxy,因此也就在内部调用的时候缓存注解会失效,而aspectj的AOP则可以解决这个问题。

proxy-target-class

proxyTargetClass

false

仅适用于proxy模式,控制类上的注解@Cacheable或@CacheEvict采用哪种缓存代理。如果proxy-target-class属性设置为true,那么将创建基于类的代理。 如果proxy-target-class为false,那么将创建基于标准JDK接口的代理。具体可以参考AOP的proxy-target-class属性

order

order

Ordered.LOWEST_PRECEDENCE

确定bean注解中@Cacheable或@CacheEvict中cache advice的顺序,没有指定则意味着使用AOP决定的advice顺序。具体可以参考A

时间: 2024-10-30 21:19:36

java中利用spring cache解耦业务中的缓存的相关文章

利用 Spring Boot 在 Docker 中运行 Hadoop

本文讲的是利用 Spring Boot 在 Docker 中运行 Hadoop,[编者的话]Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.本文介绍了如何利用Spring Boot在Docker中运行Hadoop任务. 简介 越来越多的应用都开始使用Hadoop框架.而开发者在使用过程中也遇到一些挑战,比如使用诸如Docker之类的容器开发和部署相关的技术栈开发的应用.我们将会在下面的例子中介绍如何克服这些挑战. 由于 S

指针-C++中利用子函数交换main()中的一个int数组的值,交换地址为什么不可?

问题描述 C++中利用子函数交换main()中的一个int数组的值,交换地址为什么不可? 某书思考题 只改动子函数 实现主函数中数组排序我写了3种子函数 注释的都是可以正常用的 最上面的不可以(排序没变)不知道是为啥(指针不是代表地址吗 既然形参无法传回那我改变地址应该也可以啊) #include<iostream>#include<iomanip>#include<cstdlib> //pauseusing namespace std;//排序不变void swap(

Java Web程序中利用Spring框架返回JSON格式的日期_java

返回Json时格式化日期Date第一步:创建CustomObjectMapper类 /** * 解决SpringMVC使用@ResponseBody返回json时,日期格式默认显示为时间戳的问题.需配合<mvc:message-converters>使用 */ @Component("customObjectMapper") public class CustomObjectMapper extends ObjectMapper { public CustomObjectM

android中data/data/cache目录

问题描述 android中data/data/cache目录 android中data/data/cache目录是做什么用的? android4.4之后还可以向这个目录中写入内容么? 解决方案 系统缓存存放在"/cache"下. 解决方案二: 主要放各种缓存文件. 解决方案三: cache : 存放缓存数据 解决方案四: swbsun说的对,系统缓存会放在/cache下. 之前我不清楚什么情况,目前android4.4.4是没有/data/data/cache这个目录的. 每个应用的缓

安卓开发-java中的spring框架是一个怎么样的框架,

问题描述 java中的spring框架是一个怎么样的框架, java中的spring框架是一个怎么样的框架, 在android开发当中有没有应用到呢 解决方案 spring最初是一个IoC框架,主要的作用是实现组件的管理.有时候我们希望程序中一些代码可以标准化并且被替换,比如一个管理系统底层可以使用SQL Server,也可以使用MySQL,那么我们编写两个符合接口的组件,Spring的作用是通过配置文件把需要的组件装配起来,比如得到一套支持mssql的系统,一套支持mysql的系统. 而主程序

Java(多)线程中注入Spring的Bean

问题说明 : 今天在web应用中用到了Java多线程的技术来并发处理一些业务,但在执行时一直会报NullPointerException的错误,问题定位了一下发现是线程中的Spring bean没有被注入,bean对象的值为null. 原因分析 : web容器在启动应用时,并没有提前将线程中的bean注入(在线程启动前,web容易也是无法感知的) 解决方案 : 线程中获取bean import org.springframework.context.ApplicationContext; pub

java struts2-怎样利用Java 中的struts2框架实现数据库中用户登录功能?

问题描述 怎样利用Java 中的struts2框架实现数据库中用户登录功能? 在Action中LoginAction怎样写? 配置文件中怎样写? 总体实现能够使数据库中已经存在的用户凭自己的密码与用户名登录成功呢?数据库是Oracle数据库.

Java中利用字符串连接解决问题

在应用程序开发过程中,使用的最多的数据类型就是字符串 .在Java语言平台中也是如此.为此掌握 字符串的处理技巧,无疑是一位数据库管理员必须要掌握的技能.笔者这里就给大家介绍如何利用字符串 连接来解决一些实际的问题. 一. 字符串连接概述. 在编写应用程序的时候,我们往往需要将多个字符串连接起来,来完成特定的功能.如现在有两个字 符串变量,分别为名字(变量名为name)和年龄(变量名为age).现在需要在屏幕上输出"我的名字是某某 ,年龄多少"这个字符串.这个语句该如何写呢?可以写为&

java中的spring配置文件中引入context命名空间却不能使用context标签

问题描述 java中的spring配置文件中引入context命名空间却不能使用context标签 myeclipse中spring配置文件的 命名空间 为什么导入了相对应的命名空间但是 相对应的标题 却不能使用 求大神 解决方案 http://zhidao.baidu.com/link?url=CYTl2LDpKwI4j4Dgi2W7vI8OltUewUkwn1A8dW_7-LDXMNPJzc2ieqDzv33rIzF4W2s2Ss5B7SeSsn_UPoGXYqCmZb-kcdotYkwhe