feat(mq-starter-idempotent-redis): mq.idempotent.* 中立配置 + 不按 mq.type 门控的去重 store 自动装配

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 03:12:06 +01:00
parent c2a31c9339
commit f558f6303e
5 changed files with 197 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
package com.njcn.mq.idempotent.redis;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link MqIdempotentProperties} 默认值守护:必须与迁移前 RedisStreamMqProperties.Idempotent
* 完全一致(processing 60s / success 300s / fail 300s / 前缀 mq:idem:),保证迁移不改变现有行为。
*/
class MqIdempotentPropertiesTest {
@Test
void defaults_matchLegacyRedisStreamIdempotentDefaults() {
MqIdempotentProperties p = new MqIdempotentProperties();
assertTrue(p.isEnabled(), "去重装配默认开启,仅显式 false 关闭");
assertEquals("mq:idem:", p.getKeyPrefix());
assertEquals(Duration.ofSeconds(60), p.getProcessingTtl());
assertEquals(Duration.ofSeconds(300), p.getSuccessTtl());
assertEquals(Duration.ofSeconds(300), p.getFailTtl());
}
}

View File

@@ -0,0 +1,85 @@
package com.njcn.mq.idempotent.redis;
import com.njcn.mq.autoconfig.MqCoreAutoConfiguration;
import com.njcn.mq.spi.MqIdempotentStore;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* 装配条件矩阵(ApplicationContextRunner,不连真实 Redis):
* 关键契约是「不按 mq.type 门控、只按 Redis 在场门控」—— rocketmq 部署也能装出 store。
*/
class MqIdempotentRedisAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MqIdempotentRedisAutoConfiguration.class));
@Test
void withStringRedisTemplate_storeAutoConfigured_regardlessOfMqType() {
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
// 故意设 mq.type=rocketmq:装配必须与 mq.type 无关(这正是 rocketmq 用上去重的关键)
.withPropertyValues("mq.type=rocketmq")
.run(ctx -> {
assertThat(ctx).hasSingleBean(MqIdempotentStore.class);
assertThat(ctx.getBean(MqIdempotentStore.class)).isInstanceOf(RedisMqIdempotentStore.class);
});
}
@Test
void withoutRedisBean_noStoreAndNoStartupFailure() {
runner.run(ctx -> {
assertThat(ctx).hasNotFailed();
assertThat(ctx).doesNotHaveBean(MqIdempotentStore.class);
});
}
@Test
void enabledFalse_disablesStore() {
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
.withPropertyValues("mq.idempotent.enabled=false")
.run(ctx -> assertThat(ctx).doesNotHaveBean(MqIdempotentStore.class));
}
@Test
void userProvidedStore_backsOff() {
MqIdempotentStore custom = mock(MqIdempotentStore.class);
runner.withBean(StringRedisTemplate.class, () -> mock(StringRedisTemplate.class))
.withBean("customStore", MqIdempotentStore.class, () -> custom)
.run(ctx -> {
assertThat(ctx).hasSingleBean(MqIdempotentStore.class);
assertThat(ctx.getBean(MqIdempotentStore.class)).isSameAs(custom);
});
}
@Test
void declaresAutoConfigureOrderContract() {
// 真实 Spring Boot 按类名字母序处理 autoconfig:com.njcn.mq.autoconfig(core) 先于
// com.njcn.mq.idempotent(本模块)。显式声明顺序契约,守护 spec §4.4 的装配语义。
AutoConfigureBefore before = MqIdempotentRedisAutoConfiguration.class.getAnnotation(AutoConfigureBefore.class);
assertThat(before).as("必须声明 @AutoConfigureBefore(MqCoreAutoConfiguration)").isNotNull();
assertThat(before.value()).contains(MqCoreAutoConfiguration.class);
AutoConfigureAfter after = MqIdempotentRedisAutoConfiguration.class.getAnnotation(AutoConfigureAfter.class);
assertThat(after).as("必须声明 @AutoConfigureAfter(RedisAutoConfiguration),否则 @ConditionalOnBean(StringRedisTemplate) 求值时模板尚未注册").isNotNull();
assertThat(after.value()).contains(RedisAutoConfiguration.class);
}
@Test
void registeredInSpringFactories() {
List<String> names = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
getClass().getClassLoader());
assertThat(names).contains(MqIdempotentRedisAutoConfiguration.class.getName());
}
}