如何阻止spring-data-redis索引实体特定字段的嵌套属性?
我尝试使用spring-data-redis将包含javax.money.MonetaryAmount类型字段的实体保存到Redis中,实体类代码如下:
@RedisHash("transaction") class Transaction( @org.springframework.data.annotation.Id val id: Id, val description: String, val amount: MonetaryAmount ) { class Id(val value: UUID) { companion object { fun random(): Id = Id(UUID.randomUUID()) } } }
问题在于javax.money.MonetaryAmount接口包含MonetaryAmountFactory<? extends MonetaryAmount> getFactory();方法,Spring会将该方法识别为属性。尝试缓存这个属性时,抛出以下异常:
org.springframework.data.keyvalue.core.UncategorizedKeyValueException: No getter available for persistent property javax.money.MonetaryAmountFactory.number
调试代码后发现,问题出在MappingRedisConverter.write(object,sink)中的这一行:
Iterator var6 = this.indexResolver.resolveIndexesFor(entity.getTypeInformation(), source).iterator();
此时:
- Redis已通过自定义转换器完成键的写入
- Redis已通过自定义转换器完成值的写入
- 问题根源是Spring尝试查找需要索引的嵌套属性时出错
我已经配置了自定义序列化器,但没有效果——这是合理的,因为问题不在序列化环节,而在索引环节。自定义配置代码如下:
@Configuration @EnableRedisRepositories class RedisConfiguration { @Bean fun redisTemplate(connectionFactory: RedisConnectionFactory?): RedisTemplate<String, Any>? { val template = RedisTemplate<String, Any>() template.connectionFactory = connectionFactory val entitySerializer = GenericJackson2JsonRedisSerializer(objectMapper()) template.setDefaultSerializer(entitySerializer) template.valueSerializer = entitySerializer template.hashKeySerializer = entitySerializer template.hashValueSerializer = entitySerializer return template } @Bean fun objectMapper(): ObjectMapper { val objectMapper = ObjectMapper() objectMapper.registerModule(Jdk8Module()) objectMapper.registerModule(JavaTimeModule()) objectMapper.registerModule(MoneyModule()) return objectMapper } @Bean fun redisConverter( mappingContext: RedisMappingContext?, customConversions: RedisCustomConversions?, referenceResolver: ReferenceResolver? ): MappingRedisConverter? { val mappingRedisConverter = MappingRedisConverter( mappingContext, null, referenceResolver, DefaultRedisTypeMapper() ) mappingRedisConverter.setCustomConversions(redisCustomConversions()) return mappingRedisConverter } @Bean fun redisCustomConversions(): RedisCustomConversions? { return RedisCustomConversions( mutableListOf( ByteArrayToTransactionIdConverter(), TransactionIdToByteArrayConverter(), StringToTransactionIdConverter(), TransactionIdToStringConverter(), ByteArrayToMonetaryAmountConverter(), MonetaryAmountToByteArrayConverter(), ) ) } } @Component @ReadingConverter class ByteArrayToTransactionIdConverter : Converter<ByteArray, Transaction.Id> { override fun convert(source: ByteArray) = source.toString(Charsets.UTF_8) .let(UUID::fromString) .let(Transaction::Id) } @Component @WritingConverter class TransactionIdToByteArrayConverter : Converter<Transaction.Id, ByteArray> { override fun convert(source: Transaction.Id) = source.value.toString().toByteArray() } @Component @WritingConverter class TransactionIdToByteArrayConverter : Converter<Transaction.Id, ByteArray> { override fun convert(source: Transaction.Id) = source.value.toString().toByteArray() } @Component @ReadingConverter class StringToTransactionIdConverter : Converter<String, Transaction.Id> { override fun convert(source: String) = Transaction.Id(UUID.fromString(source)) } @Component @WritingConverter class TransactionIdToStringConverter : Converter<Transaction.Id, String> { override fun convert(source: Transaction.Id) = source.value.toString() } @Component @WritingConverter class ByteArrayToMonetaryAmountConverter : Converter<ByteArray, MonetaryAmount> { override fun convert(source: ByteArray) = MonetaryFormats.getAmountFormat(Locale.US).parse(source.toString(Charsets.UTF_8)) } @Component @WritingConverter class MonetaryAmountToByteArrayConverter : Converter<MonetaryAmount, ByteArray> { override fun convert(source: MonetaryAmount) = source.toString().toByteArray(Charsets.UTF_8) }
理想情况下,我想把MonetaryAmountFactory.getNumber()标记为transient,但这是接口方法,我无法修改它,也不能添加Spring-Data的跨依赖。
是否有办法阻止spring-data-redis索引某个实体字段的嵌套属性?
1. 自定义RedisMappingContext排除指定属性
通过自定义RedisMappingContext,手动标记MonetaryAmount中的factory属性为transient,让Spring Data Redis不将其视为可索引的属性:
@Bean fun redisMappingContext(): RedisMappingContext { val context = RedisMappingContext() context.setInitialEntitySet(setOf(Transaction::class.java)) // 排除MonetaryAmount的factory属性 context.getPersistentEntity(MonetaryAmount::class.java)?.let { entity -> entity.getPersistentProperty("factory")?.isTransient = true } return context }
2. 替换自定义IndexResolver跳过嵌套索引
直接替换默认的索引解析器,对MonetaryAmount类型字段跳过嵌套属性索引:
@Component class CustomIndexResolver : IndexResolver { private val delegate = DefaultIndexResolver() override fun resolveIndexesFor(persistentEntity: PersistentEntity<*, *>, value: Any): Collection<IndexInformation> { // 对MonetaryAmount类型不生成任何索引 if (value is MonetaryAmount) { return emptyList() } // 其他类型沿用默认逻辑 return delegate.resolveIndexesFor(persistentEntity, value) } }
然后在redisConverter Bean中注入这个自定义解析器:
@Bean fun redisConverter( mappingContext: RedisMappingContext?, customConversions: RedisCustomConversions?, referenceResolver: ReferenceResolver?, indexResolver: CustomIndexResolver ): MappingRedisConverter? { val mappingRedisConverter = MappingRedisConverter( mappingContext, indexResolver, referenceResolver, DefaultRedisTypeMapper() ) mappingRedisConverter.setCustomConversions(redisCustomConversions()) return mappingRedisConverter }
3. 包装MonetaryAmount为自定义值对象
创建自定义包装类封装MonetaryAmount,让Spring Data Redis只处理包装类,避免直接解析第三方接口的嵌套属性:
class MonetaryAmountWrapper(val amount: MonetaryAmount)
修改Transaction实体类:
@RedisHash("transaction") class Transaction( @org.springframework.data.annotation.Id val id: Id, val description: String, val amount: MonetaryAmountWrapper ) { class Id(val value: UUID) { companion object { fun random(): Id = Id(UUID.randomUUID()) } } }
同时更新自定义转换器适配MonetaryAmountWrapper即可。
内容的提问来源于stack exchange,提问作者W.K.S

