Переглянути джерело

test(乐收赢): 补环境变量形态实测;顺带实测出 JAVA_OPTS 关开关是空操作

b6da54a 给用户的用法是 `LESHOUYING_ENABLED=false docker compose up -d`,注入的是
**环境变量**形态;而当时那两条臂注入的是**规范化**名 leshouying.enabled。
「照这一行敲就行」这句话当时没有实测支撑 —— 补两条。

一、Binder.get(Environment) 那条:过。
LESHOUYING_ENABLED 经宽松绑定得到 leshouying.enabled=false。
量具说明写进注释了:不能直接 new SpringConfigurationPropertySource(该类非 public),
Binder.get(Environment) 内部走的正是 ConfigurationPropertySources.get,与生产同一支
适配器。配一条正对照:同一 binder 查未赋值的键必须给不出值,否则「恒为 false」也满足断言。

二、真容器那条:一开始**红**,而且红得有价值。
withPropertyValues("LESHOUYING_ENABLED=false") 关不掉开关
(Tests run: 5, Failures: 1 … expected: <0> but was: <1>)。
成因在栈里,不用推:AbstractApplicationContextRunner.run 把 withPropertyValues 交给
TestPropertyValues.applyToSystemProperties(栈帧 TestPropertyValues.java:188),落成的
是 JVM **系统属性**;而「去分隔符、全大写」这条宽松形态是
SystemEnvironmentPropertySource **专有**的,系统属性只认规范名。

⇒ 落到运维上:JAVA_OPTS=-DLESHOUYING_ENABLED=false 是空操作,症状与「压根没写这一行」
   一模一样。compose 里 JAVA_OPTS 就在开关上面几行,顺手写成 -D 很自然,故在
   docker-compose.yml 就地加三行注释挡一下。
   改成 withInitializer 注入真的 SystemEnvironmentPropertySource 后 5/5 绿。
   佐证该源确实被读到(不是「因为别的原因没装配」):本类共 5 条臂,其中只有两条把开关
   置 false,而运行输出里 validate() 的 WARN 恰好两条,一一对应。

静息状态无生产代码变化:本次只加测试与注释,未碰任何 main 下的类。
全量 900 绿(此前 898,新增 2 = Binder 那条 + 真容器那条;旧的那条 withPropertyValues
臂被替换而非新增);PaymentApplicationContextTest 仍绿。

未做(同 b6da54a,如实记):isDevProfile() 的 `prod,dev` 漏洞未收紧。
alphaH 2 тижнів тому
батько
коміт
418487d941

+ 3 - 0
docker-compose.yml

@@ -14,6 +14,9 @@ services:
       # 应用照常启动(否则会被 LeshouyingProperties 的文档测试配置闸挡住,起不来)。
       # 用法:LESHOUYING_ENABLED=false docker compose up -d
       LESHOUYING_ENABLED: "${LESHOUYING_ENABLED:-true}"
+      # 关通道只走上面这一行。JAVA_OPTS 里写 -DLESHOUYING_ENABLED=false 是空操作,
+      # 实测 2026-09-28:全大写加下划线那条宽松形态是环境变量专有的,系统属性只认
+      # leshouying.enabled。症状与「压根没写这一行」一模一样,别踩。
       JAVA_OPTS: "-Xms256m -Xmx512m"
       SPRING_DATASOURCE_DRUID_URL: jdbc:postgresql://postgres:5432/payment_platform_java?currentSchema=public
       SPRING_DATASOURCE_DRUID_USERNAME: admin

+ 68 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/config/LeshouyingChannelSwitchWiringTest.java

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