自定义过滤器
缓存友好
基本概念
在全局过滤器一文中,我们介绍了自定义全局过滤器需要实现Filter/KFilter接口。
然而,使用该接口定义的普通过滤器并不是缓存友好的。
以Book实体为例,如果为其设置缓存不友好的全局过滤器,将会导致以下所有对过滤器敏感的属性
-
以
Book作为目标类型的关联属性。比如,BookStore.books、Author.books -
依赖于上述关联属性的计算属性。比如,
BookStore.avgPrice、BookStore.newestBooks(文档没提及newestBook,可参见例子)
都无法支持缓存。
Jimmer采用CacheableFilter/KCacheableFilter接口如下接口定义缓存友好的过滤器
- Java
- Kotlin
package org.babyfish.jimmer.sql.filter;
import org.babyfish.jimmer.sql.ast.table.Props;
import org.babyfish.jimmer.sql.event.EntityEvent;
import java.util.SortedMap;
public interface CacheableFilter<P extends Props> extends Filter<P> {
SortedMap<String, Object> getParameters();
boolean isAffectedBy(EntityEvent<?> e);
}
package org.babyfish.jimmer.sql.kt.filter
import org.babyfish.jimmer.sql.event.EntityEvent
import java.util.*
interface KCacheableFilter<E: Any> : KFilter<E> {
fun getParameters(): SortedMap<String, Any>?
fun isAffectedBy(e: EntityEvent<*>): Boolean
}
该接口从Filter/KFilter接口继承,在其基础上,添加了两个新方法:
-
getParameters: 该过滤器的为多视图缓存贡献的SubKey片段。 -
isAffectedBy: 接受一个被过滤实体被修改的事件,判断当前过滤器所依赖的过滤字段是否被修改。
一个实体类型允许被多个全局过滤器处理,如果出现了多个全局过滤器
-
任何一个全局过滤器对缓存不友好,都会导致对此过滤器敏感的其他属性都无法支持缓存
因此,这些全局过滤器,要么都是缓存不友好的
Filter/KFilter,要么都是缓存友好的CacheableFilter/KCacheableFilter;二者混用没有意义。如果不小心导致这种无意义的混用,Jimmer会告诉为什么缓存未生效。
-
当所有全局过滤器都缓存友好时,所有
CacheableFilter/KCacheableFilter对象的getParameters()方法返回的数据合并在一起,作为多视图缓存的SubKey例如,实体同时被两个全局过滤器处理。一个是逻辑删除所隐含的过滤器,记为
a;另外一个是用户自定义过滤器,记为b。假如
-
a的getParameters()返回{"logicalDeleted":false} -
b的getParameters()返回{"tenant":"a"},
那么最终多视图缓存中的
SubKey为{"logicalDeleted":false,"tenant":"a"} -
定义缓存友好过滤器
在[查询篇/自定义过滤器]一文中,我们为实体定义了一个超类型TenantAware,让我们再次回顾其代码,如下
- Java
- Kotlin
@MappedSuperclass
public interface TenantAware {
String tenant();
}
@MappedSuperclass
interface TenantAware {
val tenant: String
}
任何需要支持多租户的实体类型都可以继承TenantAware,例如Book
- Java
- Kotlin
@Entity
public interface Book extends TenantAware {
...省略代码...
}
@Entity
interface Book : TenantAware {
...省略代码...
}
假设Spring上下文中有一个类型为TenantProvider的对象,其Java方法get()和kotlin属性tenant用于从当前操作者身份信息中提取所属租户。定义如下过滤器
- Java
- Kotlin
@Component
public class TenantFilter implements CacheableFilter<TenantAwareProps> {
private final TenantProvider tenantProvider;
public TenantFilter(TenantProvider tenantProvider) {
this.tenantProvider = tenantProvider;
}
@Override
public void filter(FilterArgs<TenantAwareProps> args) {
String tenant = tenantProvider.get();
if (tenant != null) {
args.where(args.getTable().tenant().eq(tenant));
}
}
@Override
public SortedMap<String, Object> getParameters() {
String tenant = tenantProvider.get();
if (tenant == null) {
return null;
}
SortedMap<String, Object> map = new TreeMap<>();
map.put("tenant", tenant);
return map;
}
@Override
public boolean isAffectedBy(EntityEvent<?> e) {
return e.isChanged(TenantAwareProps.TENANT)
}
}
@Component
class TenantFilter(
private val tenantProvider: TenantProvider
) : KCacheableFilter<TenantAware> {
override fun filter(args: KFilterArgs<TenantAware>) {
tenantProvider.tenant?.let {
args.apply {
where(table.tenant.eq(it))
}
}
}
override fun getParameters(): SortedMap<String, Any>? =
tenantProvider.tenant?.let {
sortedMapOf("tenant" to it)
}
override fun isAffectedBy(e: EntityEvent<*>): Boolean =
e.isChanged(TenantAware::tenant)
}
启用多视图缓存
普通的写法
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new CacheFactory() {
@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...省略代码...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
...省略代码...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.BOOKS.unwrap() ||
prop == AuthorProps.BOOKS.unwrap()
prop,
Duration.ofMinutes(5),
Duration.ofHours(5),
);
}
@Override
public Cache<?, ?> createResolverCache(ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.AVG_PRICE.unwrap() ||
prop == BookStoreProps.NEWEST_BOOKS.unwrap()
prop,
Duration.ofHours(1),
Duration.ofHours(24)
);
}
private <K, V> Cache<K, V> createPropCache(
boolean isMultiviewCache,
ImmutableProp prop,
Duration caffeineDuration,
Duration redisDuration
) {
if (isMultiView) {
return new ChainCacheBuilder<K, V>()
.add(
CaffeineHashBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisHashBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
return new ChainCacheBuilder<>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(128)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
};
}
@Bean
fun cacheFactory(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): KCacheFactory {
return object: KCacheFactory {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
...省略代码...
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
...省略代码...
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
prop === BookStore::books.toImmutableProp() ||
prop === Author::books.toImmutableProp(),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
override fun createResolverCache(prop: ImmutableProp): Cache<*, *> =
createPropCache(
prop === BookStore::avgPrice.toImmutableProp() ||
prop === BookStore::newestBooks.toImmutableProp(),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
private fun <K, V> createPropCache(
isMultiView: Boolean,
prop: ImmutableProp,
duration: Duration
): Cache<K, V> {
if (isMultiView) {
return ChainCacheBuilder<K, V>()
.add(
CaffeineHashBinder
.forProp(prop)
.maximumSize(128)
.duration(caffeineDuration)
.build()
)
.add(
RedisHashBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build()
}
}
}
上面的代码中RedisHashBinder是非常重要的实现类,利用redis支持多视图缓存,其背后存储结构对应Redis Hashes,即,嵌套的Hash结构。
| 缓存技术风格 | 是否多视图 | 抽象接口 | 内置实现 |
|---|---|---|---|
支持自Loading的缓存 (通常是一级缓存技术:比如Guava, Caffeine) | 单视图 | LoadingBinder<K, V> | CaffeineValueBinder<K, V> |
多视图 | LoadingBinder.Parameterized<K, V> | 无 | |
不支持自Loading的缓存 (通常是二级缓存技术:比如Redis) | 单视图 | SimpleBinder<K, V> | RedisValueBinder<K, V> |
多视图 | SimpleBinder.Parameterized<K, V> | CaffeineHashBinder<K, V> | |
| RedisHashBinder<K, V> |
更好的写法
在上面的代码中,createAssociatedIdListCache方法内部对参数prop进行判断,以决定究竟应该构建多视图缓存还是单视图缓存。然而
就关联属性而言,是否需要构建多视图缓存的唯一判断依据是目标实体是否被施加了过滤器,Jimmer为此提供了更好的支持。
开发人员只需要把超接口CacheFactory/KCacheFactory替换为超类AbstractCacheFactory/AbstractKCacheFactory,
即可继承一个叫做getFilterState/filterState的成员,它可以帮助我们判断是否应该构建多视图缓存。
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new AbstractCacheFactory() {
@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...省略代码...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
getFilterState().isAffectedBy(prop.getTargetType()),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
);
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
getFilterState().isAffectedBy(prop.getTargetType()),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
);
}
@Override
public Cache<?, ?> createResolverCache(ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.AVG_PRICE.unwrap() ||
prop == BookStoreProps.NEWEST_BOOKS.unwrap()
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
);
}
private <K, V> Cache<K, V> createPropCache(
boolean isMultiviewCache,
ImmutableProp prop,
Duration caffeineDuration,
Duration redisDuration
) {
...省略代码...
}
};
}
@Bean
fun cacheFactory(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): KCacheFactory {
return object: AbstractKCacheFactory() {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
...省略代码...
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
createPropCache(
filterState.isAffectedBy(prop.targetType),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
filterState.isAffectedBy(prop.targetType),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
override fun createResolverCache(prop: ImmutableProp): Cache<*, *> =
createPropCache(
prop === BookStore::avgPrice.toImmutableProp() ||
prop === BookStore::newestBooks.toImmutableProp(),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
private fun <K, V> createPropCache(
isMultiView: Boolean,
prop: ImmutableProp,
caffeineDuration: Duration,
redisDuration: Duration
): Cache<K, V> {
...省略代码...
}
}
}
很遗憾,这种方法仅能用于简化关联缓存构建,即,简化createAssociatedIdCache和createAssociatedIdListCache方法。
对于计算属性而言,由于框架对用户自定义计算属性的内部逻辑一无所知,无法简化,用户需根据自己业务特点自行决定是否需要构建多视图缓存。