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

    xiaoxiao2021-03-25  62

    摘要: 本文讲的是java中利用spring cache解耦业务中的缓存, 虽然以前实现缓存的方式,是定义了缓存操作接口,可以灵活实现不同的缓存,可毕竟精力有限,要完成不同的缓存实现也是件麻烦的事。更要命的是,业务代码中有大量缓存操作的代码,耦合度太高,看着很不优雅。 所以呢,抽空了解了一下其它实现方案。

    云计算  云服务器ECS  大数据  建站  备案  文档  域名  whois查询  文集

    虽然以前实现缓存的方式,是定义了缓存操作接口,可以灵活实现不同的缓存,可毕竟精力有限,要完成不同的缓存实现也是件麻烦的事。更要命的是,业务代码中有大量缓存操作的代码,耦合度太高,看着很不优雅。 所以呢,抽空了解了一下其它实现方案。这不,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

    以上是云栖社区小编为您精心准备的的内容,在云栖社区的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索缓存 , 参数 , 代码 , cache , 配置 属性 spring mvc cache缓存、spring cache缓存过期、spring cache缓存字典、spring缓存 设置cache、spring cache 缓存,以便于您获取更多的相关知识。

    转载请注明原文地址: https://ju.6miu.com/read-40753.html

    最新回复(0)