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 index c522ef6..9c44bb9 100644 --- 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 @@ -2,7 +2,13 @@ package com.njcn.mq.autoconfig; import org.springframework.core.env.Environment; -/** MQ 启动校验的公共工具:统一读取逃生开关 {@code mq.startup-check.enabled}(默认 true)。 */ +/** + * MQ 启动校验的公共工具:统一读取逃生开关 {@code mq.startup-check.enabled}(默认 true)。 + * + *
只接受严格的 {@code true}/{@code false}(忽略大小写、去首尾空白)。仅当显式配为 + * {@code false} 时才关闭校验;缺省或任何非法值(如 {@code disabled})都按默认 {@code true} + * 继续校验——避免逃生开关自身因非法布尔值在启动期抛类型转换异常,反而把启动炸掉。 + */ public final class MqStartupChecks { private MqStartupChecks() { @@ -10,7 +16,7 @@ public final class MqStartupChecks { /** @return true 表示应执行启动校验;false 表示使用者主动关闭。 */ public static boolean enabled(Environment env) { - return Boolean.TRUE.equals( - env.getProperty("mq.startup-check.enabled", Boolean.class, Boolean.TRUE)); + String value = env.getProperty("mq.startup-check.enabled"); + return value == null || !"false".equalsIgnoreCase(value.trim()); } } 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 index 8b16ec7..38c043a 100644 --- 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 @@ -34,6 +34,17 @@ class MqDriverPresenceAutoConfigurationTest { .run(ctx -> assertThat(ctx).hasNotFailed()); } + @Test + void illegalEnabledValue_stillChecks_insteadOfCrashing() { + // 逃生开关填非法布尔值不应炸在类型转换上,而应按默认继续校验: + // 此处无 driver,应走到中文③提示,而不是 Spring 的「Invalid boolean」类型转换异常。 + runner.withPropertyValues("mq.type=redis-stream", "mq.startup-check.enabled=disabled") + .run(ctx -> { + assertThat(ctx).hasFailed(); + assertThat(ctx).getFailure().hasMessageContaining("没有装配任何 MqDriver"); + }); + } + @Test void disabled_skipsCheck() { runner.withPropertyValues("mq.type=redis-stream", "mq.startup-check.enabled=false") diff --git a/mq-starter-redis-stream/src/main/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfiguration.java b/mq-starter-redis-stream/src/main/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfiguration.java index 3b36fc9..1539caa 100644 --- a/mq-starter-redis-stream/src/main/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfiguration.java +++ b/mq-starter-redis-stream/src/main/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfiguration.java @@ -46,11 +46,19 @@ public class RedisStreamStartupProbeAutoConfiguration implements InitializingBea } } - /** 尽力取 host:port 用于提示;取不到(非 Lettuce / cluster / sentinel)则回退 unknown,不再抛新异常。 */ + /** 尽力取 host:port 用于提示;取不到(非 Lettuce / cluster / sentinel)则回退到有意义的文案,不再抛新异常。 */ private String target() { try { if (connectionFactory instanceof LettuceConnectionFactory) { LettuceConnectionFactory lf = (LettuceConnectionFactory) connectionFactory; + // cluster/sentinel 模式下 getHostName()/getPort() 返回的是 standalone 默认值 localhost:6379 + //(使用者从未配置过的假地址),会把排障引偏,故回退到指向真实配置项的文案。 + if (lf.isClusterAware()) { + return "集群模式,见 spring.data.redis.cluster.nodes"; + } + if (lf.isRedisSentinelAware()) { + return "哨兵模式,见 spring.data.redis.sentinel"; + } return lf.getHostName() + ":" + lf.getPort(); } } catch (Exception ignore) { diff --git a/mq-starter-redis-stream/src/test/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfigurationTest.java b/mq-starter-redis-stream/src/test/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfigurationTest.java index 5f294ca..5062577 100644 --- a/mq-starter-redis-stream/src/test/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfigurationTest.java +++ b/mq-starter-redis-stream/src/test/java/com/njcn/mq/driver/redis/RedisStreamStartupProbeAutoConfigurationTest.java @@ -3,8 +3,11 @@ package com.njcn.mq.driver.redis; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -44,6 +47,47 @@ class RedisStreamStartupProbeAutoConfigurationTest { }); } + @Test + void pingFails_messageContainsHostPort() { + // 用真实 LettuceConnectionFactory(standalone) 覆盖 target() 的 host:port 提示分支; + // override getConnection() 让探测立即失败,无需真实 Redis / 网络。 + LettuceConnectionFactory factory = + new LettuceConnectionFactory(new RedisStandaloneConfiguration("myhost", 1234)) { + @Override + public RedisConnection getConnection() { + throw new RuntimeException("Connection refused"); + } + }; + + runner.withPropertyValues("mq.type=redis-stream") + .withBean(RedisConnectionFactory.class, () -> factory) + .run(ctx -> { + assertThat(ctx).hasFailed(); + assertThat(ctx).getFailure().hasMessageContaining("myhost:1234"); + }); + } + + @Test + void clusterMode_avoidsFakeLocalhostInMessage() { + // cluster 模式下 getHostName()/getPort() 会返回 standalone 默认值 localhost:6379(假地址); + // 修复后应回退到指向 cluster 配置项的文案,而不是打印使用者从未配过的 localhost:6379。 + LettuceConnectionFactory factory = + new LettuceConnectionFactory( + new RedisClusterConfiguration(java.util.Collections.singletonList("127.0.0.1:7000"))) { + @Override + public RedisConnection getConnection() { + throw new RuntimeException("Connection refused"); + } + }; + + runner.withPropertyValues("mq.type=redis-stream") + .withBean(RedisConnectionFactory.class, () -> factory) + .run(ctx -> { + assertThat(ctx).hasFailed(); + assertThat(ctx).getFailure().hasMessageContaining("集群模式"); + }); + } + @Test void disabled_skipsProbe() { RedisConnectionFactory factory = mock(RedisConnectionFactory.class); diff --git a/mq-starter-rocketmq/src/main/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfiguration.java b/mq-starter-rocketmq/src/main/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfiguration.java index 57b35a7..2704f29 100644 --- a/mq-starter-rocketmq/src/main/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfiguration.java +++ b/mq-starter-rocketmq/src/main/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfiguration.java @@ -3,6 +3,7 @@ package com.njcn.mq.driver.rocketmq; import com.njcn.mq.autoconfig.MqStartupChecks; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; @@ -31,7 +32,10 @@ public class RocketMqStartupProbeAutoConfiguration implements InitializingBean { if (!MqStartupChecks.enabled(env)) { return; } - String nameServer = env.getProperty("rocketmq.name-server"); + // 与 driver 同源取值:driver 走 RocketMQProperties(@ConfigurationProperties)的 getNameServer(), + // 享受 Spring relaxed binding;这里也用 Binder,避免 env.getProperty 精确键查找漏掉 + // camelCase(rocketmq.nameServer)等 relaxed 变体,从而把「其实配好了」误判成「未配置」而错误 fail-fast。 + String nameServer = Binder.get(env).bind("rocketmq.name-server", String.class).orElse(null); if (!StringUtils.hasText(nameServer)) { throw new IllegalStateException( "MQ 启动失败:mq.type=rocketmq,但未配置 rocketmq.name-server。" diff --git a/mq-starter-rocketmq/src/test/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfigurationTest.java b/mq-starter-rocketmq/src/test/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfigurationTest.java index 7d5989e..2a6084d 100644 --- a/mq-starter-rocketmq/src/test/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfigurationTest.java +++ b/mq-starter-rocketmq/src/test/java/com/njcn/mq/driver/rocketmq/RocketMqStartupProbeAutoConfigurationTest.java @@ -25,6 +25,19 @@ class RocketMqStartupProbeAutoConfigurationTest { } } + @Test + void reachableNameServer_camelCaseKey_startsUp() throws Exception { + // driver 通过 RocketMQProperties 走 relaxed binding,camelCase 的 rocketmq.nameServer 也能生效; + // 探测取值必须与之同源,否则会把配好的 NameServer 误判为未配置而错误 fail-fast。 + try (ServerSocket server = new ServerSocket(0)) { + int port = server.getLocalPort(); + runner.withPropertyValues( + "mq.type=rocketmq", + "rocketmq.nameServer=127.0.0.1:" + port) + .run(ctx -> assertThat(ctx).hasNotFailed()); + } + } + @Test void unreachableNameServer_startupFails() { runner.withPropertyValues(