diff --git a/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfiguration.java b/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfiguration.java new file mode 100644 index 0000000..9be3c75 --- /dev/null +++ b/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfiguration.java @@ -0,0 +1,51 @@ +package com.njcn.mq.autoconfig; + +import com.njcn.mq.spi.MqDriver; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +/** + * ③ 通用校验:显式配了 {@code mq.type},却没有装配任何 {@link MqDriver}, + * 说明忘引对应 starter 或 mq.type 拼错 —— 启动失败并给中文提示。 + * + *

为何“有没有 MqDriver”就够:两个 driver 的 AutoConfiguration 都用 + * {@code @ConditionalOnProperty(name="mq.type", havingValue=...)} 按 mq.type 条件装配, + * 因此“存在 MqDriver bean”已等价于“配的 mq.type 有对应 driver”。 + * + *

仅当显式配了 {@code mq.type} 时装配;没配则完全不介入(存量零影响)。 + * 放在 core 是因为:redis/rocketmq 包没引入时其模块内的类自己都不加载,无法自证缺失; + * 只有 core 有“到底有没有 MqDriver”的全局视角。 + */ +@Configuration +@ConditionalOnProperty(name = "mq.type") +public class MqDriverPresenceAutoConfiguration implements SmartInitializingSingleton { + + private final ObjectProvider drivers; + private final Environment env; + + public MqDriverPresenceAutoConfiguration(ObjectProvider drivers, Environment env) { + this.drivers = drivers; + this.env = env; + } + + @Override + public void afterSingletonsInstantiated() { + if (!MqStartupChecks.enabled(env)) { + return; + } + String configured = env.getProperty("mq.type"); + // 用 stream() 而非 getIfAvailable():同时引多个 driver 包时不会抛 NoUniqueBeanDefinitionException + if (drivers.stream().findAny().isPresent()) { + return; // 有 driver 装配,放行 + } + throw new IllegalStateException( + "MQ 启动失败:已配置 mq.type=" + configured + ",但没有装配任何 MqDriver。\n" + + "请检查:\n" + + " 1) 是否引入对应 starter 依赖(redis-stream 需 mq-starter-redis-stream + spring-boot-starter-data-redis;\n" + + " rocketmq 需 mq-starter-rocketmq + rocketmq starter);\n" + + " 2) mq.type 是否拼写正确(支持:redis-stream / rocketmq)。"); + } +} diff --git a/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqStartupChecks.java b/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqStartupChecks.java new file mode 100644 index 0000000..c522ef6 --- /dev/null +++ b/mq-starter-core/src/main/java/com/njcn/mq/autoconfig/MqStartupChecks.java @@ -0,0 +1,16 @@ +package com.njcn.mq.autoconfig; + +import org.springframework.core.env.Environment; + +/** MQ 启动校验的公共工具:统一读取逃生开关 {@code mq.startup-check.enabled}(默认 true)。 */ +public final class MqStartupChecks { + + private MqStartupChecks() { + } + + /** @return true 表示应执行启动校验;false 表示使用者主动关闭。 */ + public static boolean enabled(Environment env) { + return Boolean.TRUE.equals( + env.getProperty("mq.startup-check.enabled", Boolean.class, Boolean.TRUE)); + } +} diff --git a/mq-starter-core/src/main/resources/META-INF/spring.factories b/mq-starter-core/src/main/resources/META-INF/spring.factories index 8b4edd5..ab07bf9 100644 --- a/mq-starter-core/src/main/resources/META-INF/spring.factories +++ b/mq-starter-core/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -com.njcn.mq.autoconfig.MqCoreAutoConfiguration +com.njcn.mq.autoconfig.MqCoreAutoConfiguration,\ +com.njcn.mq.autoconfig.MqDriverPresenceAutoConfiguration diff --git a/mq-starter-core/src/test/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfigurationTest.java b/mq-starter-core/src/test/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfigurationTest.java new file mode 100644 index 0000000..8b16ec7 --- /dev/null +++ b/mq-starter-core/src/test/java/com/njcn/mq/autoconfig/MqDriverPresenceAutoConfigurationTest.java @@ -0,0 +1,42 @@ +package com.njcn.mq.autoconfig; + +import com.njcn.mq.spi.MqDriver; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class MqDriverPresenceAutoConfigurationTest { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MqDriverPresenceAutoConfiguration.class)); + + @Test + void noMqType_doesNotFail() { + runner.run(ctx -> assertThat(ctx).hasNotFailed()); + } + + @Test + void mqTypeButNoDriver_fails() { + runner.withPropertyValues("mq.type=redis-stream") + .run(ctx -> { + assertThat(ctx).hasFailed(); + assertThat(ctx).getFailure().hasMessageContaining("redis-stream"); + }); + } + + @Test + void mqTypeWithDriver_ok() { + runner.withPropertyValues("mq.type=redis-stream") + .withBean("someDriver", MqDriver.class, () -> mock(MqDriver.class)) + .run(ctx -> assertThat(ctx).hasNotFailed()); + } + + @Test + void disabled_skipsCheck() { + runner.withPropertyValues("mq.type=redis-stream", "mq.startup-check.enabled=false") + .run(ctx -> assertThat(ctx).hasNotFailed()); + } +}