You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何阻止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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 03:34:51