Spring Boot2.0以后版本,去除了Guava Cache的支持,改而转向更高性能(官方说法)的咖啡因(Caffeine),前面我们学习了自定义注解Redis的失效时间,这次我们将仍以自定义注解方式讨论Caffeine Cache结合Spring-Data-Jpa的失效时间.
自定义注解:
@Target(AnnotationTarget.FUNCTION,AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Inherited
@Cacheable
annotation class CaffeineCacheable(
val cacheNames: Array<String> = [],
//val key: String = "",//自动获取参数key值
val expireTime: Long = 0 //min
)
该注解可加持在类上,在接下来的注解处理器中暂未支持对类注解的拦截,有兴趣可自己玩一下.依旧说明的是,key值在这里暂未设置,Spring Cacheable 将自动获取参数数据,来拼接key值,如有其它需求可自定义处理key值.
注解处理器:
package com.github.caffeine
import com.github.benmanes.caffeine.cache.Caffeine
import org.springframework.beans.factory.InitializingBean
import org.springframework.cache.annotation.AnnotationCacheOperationSource
import org.springframework.cache.caffeine.CaffeineCacheManager
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
import org.springframework.core.annotation.AnnotationUtils
import org.springframework.util.ReflectionUtils
import spring.data.redis.extension.isNotNull
import spring.data.redis.extension.isNull
import java.lang.reflect.Method
import java.util.concurrent.TimeUnit
class CaffeineProcessor: CaffeineCacheManager(),InitializingBean,ApplicationContextAware {
private var applicationContext: ApplicationContext? = null
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
override fun afterPropertiesSet() {
parseCacheDuration(applicationContext)
}
private fun createdCaffeine(expireTime: Long) = Caffeine.newBuilder().expireAfterAccess(expireTime,TimeUnit.MINUTES)
private fun buildCaffeineCache(name: String,expireTime: Long){
super.setCaffeine(this.createdCaffeine(expireTime))
super.createCaffeineCache(name)
}
private fun findCaffeineCacheable(clazz: Class<*>){
AnnotationCacheOperationSource()
ReflectionUtils.doWithMethods(clazz, { method ->
ReflectionUtils.makeAccessible(method)
val caffeineCache = findCaffeineCache(method)
if (caffeineCache.isNotNull()) {
buildCaffeineCache(caffeineCache.cacheNames[0],caffeineCache.expireTime)
}
}, { method ->
AnnotationUtils.findAnnotation(method, CaffeineCacheable::class.java).isNotNull()
})
}
private fun parseCacheDuration(applicationContext: ApplicationContext?) {
if (applicationContext.isNull()) return
val beanNames = applicationContext!!.getBeanNamesForType(Any::class.java)
beanNames.forEach {
val clazz = applicationContext!!.getType(it)
findCaffeineCacheable(clazz)
}
}
private fun findCaffeineCache(method: Method) = AnnotationUtils.findAnnotation(method, CaffeineCacheable::class.java)
}
我在创建Caffeine时使用的是基于时间(Time-Based)定时驱逐策略中的expireAfterAccess,该方法是:在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期,失效单位时间为min;这里简单说明下Caffeine其他另外两种定时驱逐策略:
1.expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期.
2.expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算.
代码中实现InitializingBean接口其实是在学习Redis时get到的技能,其目的是启动SpringBoot自动初始化加载自定义注解处理器.实现拦截方法上的注解,以便获取信息.
在构建Caffeine Cache是暂未设置CacheLoader,在继承了CaffeineCacheManager中未设置的CacheLoader会默认构建
源码:
protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
if (this.cacheLoader != null) {
return this.cacheBuilder.build(this.cacheLoader);
}
else {
return this.cacheBuilder.build();
}
}
@Nonnull
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
requireWeightWithWeigher();
requireNonLoadingCache();
@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
return isBounded() || refreshes()
? new BoundedLocalCache.BoundedLocalManualCache<>(self)
: new UnboundedLocalCache.UnboundedLocalManualCache<>(self);
}
有些文档指出,不设置cacheLoader,并无此key缓存时,Caffeine将会返回null,而在这里Spring会自动将为您的查询结果做缓存;
注解应用:
import com.github.caffeine.CaffeineCacheable
import spring.data.redis.entity.Person
interface PersonCaffeineCacheRepository: BaseRedisCacheRepository<Person,String> {
@CaffeineCacheable(cacheNames = ["caffeineForName"], expireTime = 1)
fun findByName(name: String): Person?
}
为了方便将CaffeineCacheable的cacheNames传递给Cacheable在此定义了同一类型.
测试结果:
我们可以看到第一次查询发送了sql语句,而第二次则直接从缓存中获取.