|
|
@@ -13,8 +13,13 @@ import org.junit.jupiter.api.Test;
|
|
|
import org.mockito.Mockito;
|
|
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
|
+import org.springframework.boot.context.properties.bind.Binder;
|
|
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
|
|
+import org.springframework.context.ApplicationContextInitializer;
|
|
|
+import org.springframework.context.ConfigurableApplicationContext;
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
+import org.springframework.core.env.StandardEnvironment;
|
|
|
+import org.springframework.core.env.SystemEnvironmentPropertySource;
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.nio.file.Files;
|
|
|
@@ -22,6 +27,7 @@ import java.nio.file.Path;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
@@ -198,6 +204,68 @@ class LeshouyingChannelSwitchWiringTest {
|
|
|
+ "而 prod 上那份配置用的是文档公开的测试密钥: " + offSwitch);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * ★ <b>用户实际要敲的那一行到底生不生效。</b>
|
|
|
+ *
|
|
|
+ * <p>上面那条臂注入的是**规范化**属性名 `leshouying.enabled`;而 compose 里那一行是
|
|
|
+ * {@code LESHOUYING_ENABLED: "${LESHOUYING_ENABLED:-true}"} —— **环境变量形态**。
|
|
|
+ * 两者之间隔着 Spring 的宽松绑定:绑不上的话,线上那句开关就是空操作,
|
|
|
+ * 而症状(应用照旧起不来)与「开关没写」一模一样,极难分辨。
|
|
|
+ * 所以「照文档敲这一行就行」这句话必须**实测**,不能靠 Spring 的常识推。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void 环境变量形态_真的被规范化成_leshouying_point_enabled() {
|
|
|
+ // ★ 量具的选择:本类**不能**直接 new SpringConfigurationPropertySource
|
|
|
+ // (那个类不是 public)。改用 Binder.get(Environment) —— 它内部走的就是
|
|
|
+ // ConfigurationPropertySources.get(environment),即生产上同一支适配器:
|
|
|
+ // SystemEnvironmentPropertySource 会被配上 SystemEnvironmentPropertyMapper。
|
|
|
+ // 下面这条断言因此证的是**那条适配链**,不是我自己手搓的映射。
|
|
|
+ StandardEnvironment env = new StandardEnvironment();
|
|
|
+ env.getPropertySources().addFirst(
|
|
|
+ new SystemEnvironmentPropertySource("compose-env", Map.of("LESHOUYING_ENABLED", "false")));
|
|
|
+ assertEquals(Boolean.FALSE,
|
|
|
+ Binder.get(env).bind("leshouying.enabled", Boolean.class).orElse(null),
|
|
|
+ "环境变量 LESHOUYING_ENABLED 没有被宽松绑定成 leshouying.enabled —— "
|
|
|
+ + "那 compose 里那一行就是空操作(@ConditionalOnProperty 读的是规范化名)");
|
|
|
+
|
|
|
+ // 正对照:同一支量具对**未被赋值**的键必须给不出值,否则上面那条可能只是「恒为 false」。
|
|
|
+ assertNull(Binder.get(env).bind("leshouying.never-set", Boolean.class).orElse(null),
|
|
|
+ "量具对不存在的键也给出了值 —— 上面那条断言不可信");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同一条注入路径在**真容器**里过一遍:环境变量形态必须真的把定时任务摘掉。
|
|
|
+ *
|
|
|
+ * <p>★ 这里**不能**用 {@code withPropertyValues("LESHOUYING_ENABLED=false")}。
|
|
|
+ * 实测(2026-09-28):那样写开关**不生效** ——
|
|
|
+ * {@code Tests run: 5, Failures: 1 … expected: <0> but was: <1>}。
|
|
|
+ * 成因在栈里读得出来,不用推:{@code AbstractApplicationContextRunner.run} 把
|
|
|
+ * {@code withPropertyValues} 交给 {@code TestPropertyValues.applyToSystemProperties}
|
|
|
+ * (栈帧 {@code TestPropertyValues.java:188}),落成的是 **JVM 系统属性**;
|
|
|
+ * 而「去掉分隔符、全大写」这条宽松形态是 {@code SystemEnvironmentPropertySource}
|
|
|
+ * **专有**的,系统属性只认规范名。
|
|
|
+ *
|
|
|
+ * <p>⇒ 落到运维上是一条实打实的坑:{@code JAVA_OPTS=-DLESHOUYING_ENABLED=false}
|
|
|
+ * 关不掉开关,而症状(应用照旧被启动闸挡住)与「压根没写这一行」一模一样。
|
|
|
+ * compose 里那一行走的是容器环境变量,是通的;要关就用那条。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void 环境变量形态_真容器里也能关掉装配() {
|
|
|
+ // 必须是**环境变量源**:系统属性源在这里量的是另一件事(见上面的实测)。
|
|
|
+ ApplicationContextInitializer<ConfigurableApplicationContext> injectComposeEnv = ctx ->
|
|
|
+ ctx.getEnvironment().getPropertySources().addFirst(
|
|
|
+ new SystemEnvironmentPropertySource("compose-env",
|
|
|
+ Map.of("LESHOUYING_ENABLED", "false")));
|
|
|
+ base().withInitializer(injectComposeEnv).run(context -> {
|
|
|
+ assertNull(context.getStartupFailure(), "实际: " + chain(context.getStartupFailure()));
|
|
|
+ assertEquals(0, context.getBeanNamesForType(LsyOrderPollScheduler.class).length,
|
|
|
+ "以环境变量形态关开关时定时任务仍被注册 —— compose 那条注入路径不生效");
|
|
|
+ // 两个调度器都要摘 —— 只摘一个说明注解在某些类上没生效(覆盖面那条臂的另一半)。
|
|
|
+ assertEquals(0, context.getBeanNamesForType(LsyReconcileScheduler.class).length,
|
|
|
+ "对账任务也必须一并摘掉");
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/** 开关注解**在类上**且三个参数都对 —— {@code havingValue}/{@code matchIfMissing} 一个不对就是关不干净。 */
|
|
|
private static boolean hasSwitchOnType(String fqcn) throws ClassNotFoundException {
|
|
|
ConditionalOnProperty c = Class.forName(fqcn).getAnnotation(ConditionalOnProperty.class);
|