引言
后面咱们有学习 Caffeine
本地缓存性能之王 Caffeine,并且也提到SpringBoot
默认应用的本地缓存也是 Caffeine
啦,明天咱们来看看 Caffeine
如何与 SpringBoot
集成的。
集成 caffeine
caffeine
与 SpringBoot
集成有两种形式:
- 一种是咱们间接引入
Caffeine
依赖,而后应用Caffeine
办法实现缓存。相当于应用原生 api -
引入
Caffeine
和Spring Cache
依赖,应用SpringCache
注解办法实现缓存。SpringCache 帮咱们封装了 Caffeine
pom 文件引入<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.6.0</version> </dependency>
第一种形式
首先配置一个
Cache
,通过结构者模式构建一个Cache
对象,而后后续对于缓存的增删查都是基于这个cache
对象。@Configuration public class CacheConfig { @Bean public Cache<String, Object> caffeineCache() {return Caffeine.newBuilder() // 设置最初一次写入或拜访后通过固定工夫过期 .expireAfterWrite(60, TimeUnit.SECONDS) // 初始的缓存空间大小 .initialCapacity(100) // 缓存的最大条数 .maximumSize(1000) .build();}
第一种形式咱们就一一不介绍了,基本上就是应用
caffeineCache
来依据你本人的业务来操作以下办法
这种形式应用的话是对代码有侵入性的。第二种形式
-
须要在 SpingBoot 启动类标上
EnableCaching
注解,这个玩意跟很多框架都一样,比方咱们肴集成dubbo
也须要标上@EnableDubbole
注解等。@SpringBootApplication @EnableCaching public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args); }
-
在
application.yml
配置咱们的应用的缓存类型、过期工夫、缓存策略等。spring: profiles: active: dev cache: type: CAFFEINE caffeine: spec: maximumSize=500,expireAfterAccess=600s
如果咱们不习惯应用这种形式的配置,当然咱们也能够应用
JavaConfig
的配置形式来代替配置文件。@Configuration public class CacheConfig { @Bean public CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() // 设置最初一次写入或拜访后通过固定工夫过期 .expireAfterAccess(600, TimeUnit.SECONDS) // 初始的缓存空间大小 .initialCapacity(100) // 缓存的最大条数 .maximumSize(500)); return cacheManager; }
接下来就是代码中如何来应用这个缓存了
@Override @CachePut(value = "user", key = "#userDTO.id") public UserDTO save(UserDTO userDTO) {userRepository.save(userDTO); return userDTO; } @Override @CacheEvict(value = "user", key = "#id")//2 public void remove(Long id) {logger.info("删除了 id、key 为" + id + "的数据缓存"); } @Override @Cacheable(value = "user",key = "#id") public UserDTO getUserById(Long id) {return userRepository.findOne(id); }
上述代码中咱们能够看到有几个注解
@CachePut、@CacheEvict、@Cacheable
咱们只须要在办法上标上这几个注解,咱们就可能应用缓存了,咱们别离来介绍下这几个注解。@Cacheable
@Cacheable
它是既能够标注在类上也能够标注在办法上,当它标记在类上的时候它表述这个类下面的所有办法都会反对缓存,同样的
当它作用在法下面时候它示意这个办法是反对缓存的。比方下面咱们代码中的getUserById
这个办法第一次缓存外面没有数据,咱们会去查问DB
,然而第二次来查问的时候就不会走DB
查问了,而是间接从缓存外面拿到后果就返回了。value 属性
-
@Cacheable
的value
属性是必须指定的,其示意以后办法的返回值是会被缓存在哪个Cache
上的,对应Cache
的名称。key
-
@Cacheable
的key
有两种形式一种是咱们本人显示的去指定咱们的key
,还有一种默认的生成策略,默认的生成策略是SimpleKeyGenerator
这个类,这个生成key
的形式也比较简单咱们能够看下它的源码:public static Object generateKey(Object... params) {// 如果办法没有参数 key 就是一个 new SimpleKey() if (params.length == 0) {return SimpleKey.EMPTY;} // 如果办法只有一个参数 key 就是以后参数 if (params.length == 1) {Object param = params[0]; if (param != null && !param.getClass().isArray()) {return param;} } // 如果 key 是多个参数,key 就是 new SimpleKey,不过这个 SimpleKey 对象的 hashCode 和 Equals 办法是依据办法传入的参数重写的。return new SimpleKey(params); }
上述代码还是十分好了解的分为三种状况:
- 办法没有参数,那就 new 应用一个全局空的
SimpleKey
对象来作为key
。 - 办法就一个参数,就应用以后参数来作为
key
-
办法参数大于
1
个,就new
一个SimpleKey
对象来作为key
,new
这个SimpleKey
的时候用传入的参数重写了SimpleKey
的hashCode
和equals
办法,
至于为啥须要重写的起因话,就跟Map
用自定义对象来作为key
的时候必须要重写hashCode
和equals
办法原理是一样的,因为caffein
也是借助了ConcurrentHashMap
来实现,小结
上述代码咱们能够发现默认生成
key
只跟咱们传入的参数有关系,如果咱们有一个类外面如果存在多个没有参数的办法,而后咱们应用了默认的缓存生成策略的话,就会造成缓存失落。
或者缓存互相笼罩,或者还有可能会产生ClassCastException
因为都是应用同一个key
。比方上面这代码就会产生异样(ClassCastException
)@Cacheable(value = "user") public UserDTO getUser() {UserDTO userDTO = new UserDTO(); userDTO.setUserName("Java 金融"); return userDTO; } @Cacheable(value = "user") public UserDTO2 getUser1() {UserDTO2 userDTO2 = new UserDTO2(); userDTO2.setUserName2("javajr.cn"); return userDTO2; }
所以个别不怎么举荐应用默认的缓存生成
key
的策略。如果非要用的话咱们最好本人重写一下,带上办法名字等。相似于如下代码:@Component public class MyKeyGenerator extends SimpleKeyGenerator { @Override public Object generate(Object target, Method method, Object... params) {Object generate = super.generate(target, method, params); String format = MessageFormat.format("{0}{1}{2}", method.toGenericString(), generate); return format; }
自定义 key
咱们能够通过
Spring
的 EL 表达式来指定咱们的key
。这里的 EL 表达式能够应用办法参数及它们对应的属性。
应用办法参数时咱们能够间接应用“# 参数名
”或者“#p 参数 index
”这也是咱们比拟举荐的做法:@Cacheable(value="user", key="#id") public UserDTO getUserById(Long id) {UserDTO userDTO = new UserDTO(); userDTO.setUserName("java 金融"); return userDTO; } @Cacheable(value="user", key="#p0") public UserDTO getUserById1(Long id) {return null;} @Cacheable(value="user", key="#userDTO.id") public UserDTO getUserById2(UserDTO userDTO) {return null;} @Cacheable(value="user", key="#p0.id") public UserDTO getUserById3(UserDTO userDTO) {return null;}
@CachePut
@CachePut
指定的属性是和@Cacheable
一样的,然而它们两个是有区别的,@CachePut
标注的办法不会先去查问缓存是否有值,而是每次都会先去执行该办法,而后把后果返回,并且后果也会缓存起来。![在这里插入图片形容](https://img-blog.csdnimg.cn/ff516023113046adbf86caaea6e499f6.png)
为什么是这样的一个流程咱们能够去看看它的源码要害代码就是这一行,
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
当咱们应用办法上有 @Cacheable
注解的时候再 contexts
外面会把 CacheableOperation
退出进去,只有 contexts.get(CacheableOperation.class)取到的内容不为空的话,才会去从缓存外面取内容,否则的话 cacheHit
会间接返回 null
。至于 contexts 什么时候退出 CacheableOperation 的话咱们看下SpringCacheAnnotationParser#parseCacheAnnotations
这个办法就会明确的。具体的源码就不展现了,感兴趣的能够本人去翻。
@CacheEvict
把缓存中数据删除,用法跟后面两个注解差不多有 value 和 key 属性,须要留神一点的是它多了一个属性beforeInvocation
beforeInvocation
这个属性须要留神下它的默认值是 false,false 代表的意思是再执调用办法之前不删除缓存,只有办法执行胜利之后才会去删除缓存。设置为true
的话调用办法之前会去删除一下缓存,办法执行胜利之后还会去调用删除缓存这样就是双删了。如果办法执行异样的话就不会去删除缓存。allEntrie
是否清空所有缓存内容,默认值为false
,如果指定为true
,则办法调用后将立刻清空所有缓存
@Caching
这是一个组合注解集成了下面三个注解,有三个属性:cacheable、put 和 evict
,别离用于来指定 @Cacheable
、@CachePut
和@CacheEvict
。
小结
第二种形式是侵入式的,它的实现原理也比较简单就是通过切面的办法拦截器来实现,拦挡所有的办法,它的外围代码如下:看起来就跟咱们的业务代码差不了多少,感兴趣的也能够去瞅一瞅。
if (contexts.isSynchronized()) {CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// Process any early evictions
// beforeInvocation 属性是否为 true,如果是 true 就删除缓存
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<>();
if (cacheHit == null) {collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
完结
- 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来, 我会对其加以修改。
- 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
- 感谢您的浏览, 非常欢送并感谢您的关注。
站在伟人的肩膀上摘苹果:
https://www.cnblogs.com/fashf…!comments