当前位置: 首页 > 后端技术 > Java

SpringBoot是如何集成Caffeine的?

时间:2023-04-01 14:55:44 Java

简介前面我们了解了Caffeine,Caffeine的本地缓存性能之王,也提到了SpringBoot默认使用的本地缓存也是Caffeine。今天我们就来看看Caffeine是如何与SpringBoot集成的。caffeinecaffeine和SpringBoot的集成方式有两种:一种是我们直接引入Caffeine的依赖,然后使用Caffeine的方式实现缓存。相当于使用原生API引入Caffeine和SpringCache依赖,使用SpringCache注解方式实现缓存。SpringCache帮我们封装了Caffeinepom文件.ben-manes.caffeinecaffeine2.6.0第一种方式是先配置一个Cache,通过构造函数构建一个Cache对象模式。那么后续对缓存的增删改查都是基于这个缓存对象。@ConfigurationpublicclassCacheConfig{@BeanpublicCachecaffeineCache(){returnCaffeine.newBuilder()//设置一个固定时间在最后一次写入或access.expireAfterWrite(60,TimeUnit.SECONDS)//初始缓存的大小space.initialCapacity(100)//缓存中的最大项目数.maximumSize(1000).build();第一种方法我们就不一一介绍了,基本都是根据自己的业务使用caffeineCache来操作下面的方法,这样使用对代码有侵入性。第二种方法需要在SpingBoot启动类上标注EnableCaching注解。这个东西和很多框架一样。比如我们在集成dubbo的时候,还需要标注@EnableDubbole注解。@SpringBootApplication@EnableCachingpublicclassDemoApplication{publicstaticvoidmain(String[]args){SpringApplication.run(DemoApplication.class,args);在application.yml中配置我们的缓存类型、过期时间、缓存策略等。spring:profiles:active:devcache:type:CAFFEINEcaffeine:spec:maximumSize=500,expireAfterAccess=600s如果我们不习惯这种配置方式,当然我们也可以使用JavaConfig配置方式来代替配置文件。@ConfigurationpublicclassCacheConfig{@BeanpublicCacheManagercacheManager(){CaffeineCacheManagercacheManager=newCaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder()//设置一个固定的时间在最后一次写入或访问后过期。expireAfterAccess(600,TimeUnit.SECONDS)//初始缓存空间size.initialCapacity(100)//最大缓存项数.maximumSize(500));返回缓存管理器;}接下来就是代码中如何使用这个缓存@Override@CachePut(value="user",key="#userDTO.id")返回用户DTO;}@Override@CacheEvict(value="user",key="#id")//2publicvoidremove(Longid){logger.info("删除了带有id和key的数据缓存"+id+"");}@Override@Cacheable(value="user",key="#id")publicUserDTOgetUserById(Longid){returnuserRepository.findOne(id);在上面的代码中,我们可以看到有几个注解@CachePut,@CacheEvict,@Cacheable我们只需要在方法上标注这些注解,我们就可以使用缓存了。下面分别介绍一下这些注解。@Cacheable@Cacheable可以标注在类上,也可以标注在类的方法上。当它被标记在类上时,意味着这个类上的所有方法都将支持缓存。同样,当它作用于该方法时,则表示该方法支持缓存。比如我们上面代码中的getUserById方法,第一次缓存中没有数据,我们会查询DB,但是第二次查询不会去DB查询,而是直接从缓存中获取结果并返回向上。value属性@Cacheable的value属性必须指定,表示当前方法的返回值缓存在哪个Cache上,对应Cache的名字。key@Cacheable的key有两种方式。一种是我们自己指定我们的密钥。还有一个默认的生成策略。默认的生成策略是SimpleKeyGenerator类。这种生成密钥的方法也比较简单。我们可以看到下载它的源代码:publicstaticObjectgenerateKey(Object...params){//如果该方法没有参数键,它是一个新的SimpleKey()if(params.length==0){returnSimpleKey.空的;}//如果方法只有一个参数key是当前参数if(params.length==1){Objectparam=params[0];if(param!=null&&!param.getClass().isArray()){返回参数;}}//如果key有多个参数,则key为newSimpleKey,只是根据方法传入的参数重写了这个SimpleKey对象的hashCode和Equals方法。返回新的简单键(参数);}上面的代码还是很容易理解的,分为三种情况:方法没有参数,然后new使用一个全局空的SimpleKey对象作为key。该方法只有一个参数,当前参数作为key。方法参数大于1,新建一个SimpleKey对象作为key。new这个SimpleKey时,传入的参数重写了SimpleKey的hashCode和equals方法。至于为什么要重写的原因和Map使用自定义对象作为key时hashCode和equals方法必须重写的原理是一样的,因为caffein也是借助ConcurrentHashMap实现的。总结上面的代码,我们可以发现默认生成key只和我们传入的参数有关,如果我们在一个类中有多个没有参数的方法,然后我们使用默认的缓存生成策略,缓存就会迷路了。要么缓存相互覆盖,要么可能会出现ClassCastException,因为它们都使用相同的键。比如下面的代码会出现异常(ClassCastException)@Cacheable(value="user")publicUserDTOgetUser(){UserDTOuserDTO=newUserDTO();userDTO.setUserName("Java金融");返回用户DTO;}@Cacheable(value="user")publicUserDTO2getUser1(){UserDTO2userDTO2=newUserDTO2();userDTO2.setUserName2("javajr.cn");返回用户DTO2;所以一般不推荐使用默认的缓存生成key策略。如果非要用的话,最好自己重写,带上方法名等。类似于下面的代码:字符串格式=消息格式。格式(“{0}{1}{2}”,方法。toGenericString(),生成);返回格式;}自定义键我们可以通过Spring的EL表达式来指定我们的键。这里的EL表达式可以使用方法参数及其对应的属性。在使用方法参数时,我们可以直接使用“#parametername”或者“#pparameterindex”,这也是我们推荐的方法:@Cacheable(value="user",key="#id")publicUserDTOgetUserById(Longid){UserDTOuserDTO=newUserDTO();userDTO.setUserName("java金融");返回用户DTO;}@Cacheable(value="user",key="#p0")publicUserDTOgetUserById1(Longid){returnnull;}@Cacheable(value="user",key="#userDTO.id")publicUserDTOgetUserById2(UserDTOuserDTO){returnnull;}@Cacheable(value="user",key="#p0.id")publicUserDTOgetUserById3(UserDTOuserDTO){returnnull;}@CachePut@CachePut指定与@Cacheable相同的属性,但它们之间存在差异。@CachePut标记的方法不会先查询缓存是否有值。而是每次都会先执行方法,然后再返回结果,结果也会缓存起来。![这里插入图片描述](https://img-blog.csdnimg.cn/ff516023113046adbf86caaea6e499f6.png)为什么是这样的过程大家可以去看看其源码的关键代码是这一行,Cache.ValueWrappercacheHit=findCachedItem(contexts.get(CacheableOperation.class));当我们在方法上使用@Cacheable注解时,会在contexts中加入CacheableOperation,只有contexts.get(CacheableOperation.class)获取的内容不为空时,才会从缓存中获取内容。否则cacheHit会直接返回null。至于什么时候在CacheableOperation中加入上下文,我们看一下SpringCacheAnnotationParser#parseCacheAnnotations这个方法就明白了。具体源码就不展示了,有兴趣的可以自行翻阅。@CacheEvict删除缓存中的数据。用法和前面两个注解类似,有value和key属性。需要注意的是它多了一个属性beforeInvocationbeforeInvocation。该属性需要注意的是它的默认值为false,即false方法调用前不删除缓存,方法执行成功后才删除缓存。如果设置为true,它会在调用该方法之前删除缓存。该方法执行成功后,还会调用删除缓存,即双删除。如果方法执行异常,缓存不会被删除。allEntrie是否清除所有缓存内容,默认为false,如果指定为true,方法调用后会立即清除所有缓存@Caching这是一个结合了以上三个注解的组合注解,具有三个属性:cacheable,put和evict,分别用来指定@Cacheable、@CachePut和@CacheEvict。总结第二种方式是侵入式的,它的实现原理比较简单,就是通过切面的方法拦截器来实现,拦截所有的方法,其核心代码如下:看起来和我们的业务代码没有太大区别,有兴趣的也可以看看。if(contexts.isSynchronized()){CacheOperationContextcontext=contexts.get(CacheableOperation.class).iterator().next();if(isConditionPassing(context,CacheOperationExpressionEvaluator.NO_RESULT)){对象键=generateKey(context,CacheOperationExpressionEvaluator.NO_RESULT);缓存cache=context.getCaches().iterator().next();尝试{returnwrapCacheValue(method,cache.get(key,()->unwrapReturnValue(invokeOperation(invoker))));}catch(Cache.ValueRetrievalExceptionex){//调用者将任何Throwable包装在ThrowableWrapper实例中,这样我们//就可以确保其中一个在堆栈中冒泡。抛出(CacheOperationInvoker.ThrowableWrapper)ex.getCause();}}else{//不需要缓存,只调用底层方法返回invokeOperation(invoker);}}//Processanyearlyevictions//beforeInvocation属性是否为true,如果是true就删除缓存//检查我们是否有符合条件的缓存项Cache.ValueWrappercacheHit=findCachedItem(contexts.get(CacheableOperation.class));//如果未找到缓存项,则从任何@Cacheable未命中中收集putListcachePutRequests=newLinkedList<>();如果(cacheHit==null){collectPutRequests(contexts.get(CacheableOperation.class),CacheOperationExpressionEvaluator.NO_RESULT,cachePutRequests);}对象缓存值;对象返回值;if(cacheHit!=null&&!hasCachePut(contexts)){//如果没有putre任务,只需使用缓存命中cacheValue=cacheHit.get();returnValue=wrapCacheValue(方法,cacheValue);}else{//如果我们没有缓存命中则调用该方法returnValue=invokeOperation(invoker);cacheValue=unwrapReturnValue(returnValue);}//收集任何明确的@CachePutscollectPutRequests(contexts.get(CachePutOperation.class),cacheValue,cachePutRequests);//处理任何收集到的放置请求,来自@CachePut或@CachePutquestforRequest:cachePutRequests){cachePutRequest.apply(cacheValue);}//处理任何延迟驱逐processCacheEvicts(contexts.get(CacheEvictOperation.class),false,cacheValue);返回返回值;如果您发现错误,请留言指出给我,我会改正。如果您觉得文章还不错,您的转发、分享、欣赏、点赞、评论就是对我最大的鼓励。感谢您的阅读,非常欢迎并感谢您的关注。站在巨人的肩膀上摘苹果:https://www.cnblogs.com/fashf...!comments