feat(mq-starter-idempotent-redis): 旧键 mq.redis-stream.idempotent.* Binder 逐键回退兼容 + 聚合弃用告警

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 11:34:21 +08:00
parent f558f6303e
commit b449c26356
2 changed files with 127 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ package com.njcn.mq.idempotent.redis;
import com.njcn.mq.autoconfig.MqCoreAutoConfiguration;
import com.njcn.mq.spi.MqIdempotentStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -10,10 +11,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* 消费去重 store 自动装配(driver 中立)。
*
@@ -24,7 +31,12 @@ import org.springframework.data.redis.core.StringRedisTemplate;
* classpath 有 lettuce 时 Spring Boot 会用 localhost:6379 默认值自动造出 StringRedisTemplate,
* 因此 @ConditionalOnBean 几乎恒真,它不代表 Redis 已正确配置;真正的安全网是懒连接 +
* core 仅在 idempotent=true 时才调用 store(未开去重时 bean 造了也无人调用,零副作用)。
*
* <p><b>旧键兼容</b>:mq.redis-stream.idempotent.* 已中立化为 mq.idempotent.*。用 Binder 逐键回退
* (而非 env.getProperty 精确键,后者会漏 relaxed binding 变体):中立键显式设置→中立;否则旧键
* 显式设置→旧键值+弃用告警;否则默认。
*/
@Slf4j
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@AutoConfigureBefore(MqCoreAutoConfiguration.class)
@@ -33,12 +45,42 @@ import org.springframework.data.redis.core.StringRedisTemplate;
@EnableConfigurationProperties(MqIdempotentProperties.class)
public class MqIdempotentRedisAutoConfiguration {
private static final String NEUTRAL_PREFIX = "mq.idempotent.";
private static final String LEGACY_PREFIX = "mq.redis-stream.idempotent.";
@Bean
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnMissingBean(MqIdempotentStore.class)
public MqIdempotentStore mqIdempotentStore(StringRedisTemplate redis, MqIdempotentProperties props) {
return new RedisMqIdempotentStore(redis, props.getKeyPrefix(),
props.getProcessingTtl().getSeconds(), props.getSuccessTtl().getSeconds(),
props.getFailTtl().getSeconds());
public MqIdempotentStore mqIdempotentStore(StringRedisTemplate redis,
MqIdempotentProperties props,
Environment environment) {
Binder binder = Binder.get(environment);
List<String> usedLegacyKeys = new ArrayList<>();
String keyPrefix = resolve(binder, "key-prefix", String.class, props.getKeyPrefix(), usedLegacyKeys);
Duration processingTtl = resolve(binder, "processing-ttl", Duration.class, props.getProcessingTtl(), usedLegacyKeys);
Duration successTtl = resolve(binder, "success-ttl", Duration.class, props.getSuccessTtl(), usedLegacyKeys);
Duration failTtl = resolve(binder, "fail-ttl", Duration.class, props.getFailTtl(), usedLegacyKeys);
if (!usedLegacyKeys.isEmpty()) {
log.warn("[mq] 检测到已弃用配置 mq.redis-stream.idempotent.*,请迁移到 mq.idempotent.*;本次已按旧键取值:{}", usedLegacyKeys);
}
return new RedisMqIdempotentStore(redis, keyPrefix,
processingTtl.getSeconds(), successTtl.getSeconds(), failTtl.getSeconds());
}
/**
* 单字段回退:中立键显式设置则以中立为准(props 已按中立键绑定,字段值即中立值);
* 否则旧键显式设置则用旧键值并记入弃用清单;否则用默认(同样取 props 字段值)。
*/
private static <T> T resolve(Binder binder, String field, Class<T> type,
T neutralOrDefault, List<String> usedLegacyKeys) {
if (binder.bind(NEUTRAL_PREFIX + field, type).isBound()) {
return neutralOrDefault;
}
T legacy = binder.bind(LEGACY_PREFIX + field, type).orElse(null);
if (legacy != null) {
usedLegacyKeys.add(LEGACY_PREFIX + field);
return legacy;
}
return neutralOrDefault;
}
}