feat(mq-starter-core): 启动期校验 mq.type 有对应 MqDriver(③ fail-fast)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 10:34:50 +08:00
parent bd2e733d98
commit 5c4a29e1e1
4 changed files with 111 additions and 1 deletions

View File

@@ -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 拼错 —— 启动失败并给中文提示。
*
* <p>为何“有没有 MqDriver”就够两个 driver 的 AutoConfiguration 都用
* {@code @ConditionalOnProperty(name="mq.type", havingValue=...)} 按 mq.type 条件装配,
* 因此“存在 MqDriver bean”已等价于“配的 mq.type 有对应 driver”。
*
* <p>仅当显式配了 {@code mq.type} 时装配;没配则完全不介入(存量零影响)。
* 放在 core 是因为redis/rocketmq 包没引入时其模块内的类自己都不加载,无法自证缺失;
* 只有 core 有“到底有没有 MqDriver”的全局视角。
*/
@Configuration
@ConditionalOnProperty(name = "mq.type")
public class MqDriverPresenceAutoConfiguration implements SmartInitializingSingleton {
private final ObjectProvider<MqDriver> drivers;
private final Environment env;
public MqDriverPresenceAutoConfiguration(ObjectProvider<MqDriver> 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");
}
}

View File

@@ -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));
}
}

View File

@@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.njcn.mq.autoconfig.MqCoreAutoConfiguration com.njcn.mq.autoconfig.MqCoreAutoConfiguration,\
com.njcn.mq.autoconfig.MqDriverPresenceAutoConfiguration

View File

@@ -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());
}
}