package com.ruoyi.publicity.api; import com.ruoyi.common.core.service.IConfigService; import com.ruoyi.common.core.service.PublicityApi; import com.ruoyi.publicity.config.PublicityProperties; import com.ruoyi.publicity.config.PublicitySwitch; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.util.concurrent.atomic.AtomicInteger; /** * 宣教模块的 Spring 容器装配校验(不依赖 JUnit、不依赖数据库)。 * *

与 {@code InhospVisitContextCheck} 同一套做法:用真实的(但极简的) * {@link AnnotationConfigApplicationContext} 验证"Spring 能把 Bean 装起来"。 * 断言覆盖满意度模块实际踩过的两个坑: *

    *
  1. 门面有两个公开构造器却没标 {@code @Autowired} → 启动报 {@code No default constructor found};
  2. *
  3. 门面把自己当实现 → 调用时无限递归 → {@code StackOverflowError}。
  4. *
* *

运行方式

*
 * mvn -o -q -pl ruoyi-publicity -am test-compile -DskipTests
 * # 然后用模块 classpath 直接跑 main(surefire 认不出它,因为没有 @Test)
 * 
*/ public class PublicityContextCheck { private static int failed = 0; private static void check(String name, String expected, String actual) { if (expected.equals(actual)) { System.out.println(" [OK] " + name + " = " + actual); } else { System.out.println(" [FAIL] " + name + " 期望=[" + expected + "] 实际=[" + actual + "]"); failed++; } } /** 参数表的模拟取值 */ private static final String SYS_ON = "true"; private static final String SYS_OFF = "false"; private static final String SYS_MISSING = null; public static void main(String[] args) { System.out.println("=== 场景 1:参数表 publicity.enabled=true,且存在内部实现 ==="); try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_ON)) { PublicityFacade facade = ctx.getBean(PublicityFacade.class); check("门面能被 Spring 实例化(构造器不再歧义)", "true", String.valueOf(facade != null)); check("启用时 isEnabled()", "true", String.valueOf(facade.isEnabled())); check("取到的是【内部实现】而不是门面自己", "1", String.valueOf(facade.dealInHospPublicity("7"))); check("容器内 PublicityApi 实现数 = 门面 1 + 内部实现 1", "2", String.valueOf(ctx.getBeansOfType(PublicityApi.class).size())); check("开关读取的键名是 sys_config 的 publicity.enabled", "publicity.enabled", PublicitySwitch.CONFIG_KEY); } System.out.println(); System.out.println("=== 场景 2:参数表=true,但【没有】内部实现(模块没被打进应用)==="); try (AnnotationConfigApplicationContext ctx = context(true, false, SYS_ON)) { PublicityFacade facade = ctx.getBean(PublicityFacade.class); check("无实现时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled())); String result; try { result = String.valueOf(facade.dealInHospPublicity("7")); } catch (StackOverflowError e) { result = "StackOverflowError"; } check("无实现时入院宣教安全返回 0(不会自引用递归)", "0", result); String result2; try { result2 = String.valueOf(facade.dealMedicalPublicity("7")); } catch (StackOverflowError e) { result2 = "StackOverflowError"; } check("无实现时药品宣教安全返回 0(不会自引用递归)", "0", result2); } System.out.println(); System.out.println("=== 场景 3:参数表 publicity.enabled=false(实现 Bean 依然存在,但门面必须不转发)==="); StubApi.CALLS.set(0); try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_OFF)) { PublicityFacade facade = ctx.getBean(PublicityFacade.class); check("关闭时 isEnabled() 为 false", "false", String.valueOf(facade.isEnabled())); check("关闭时入院宣教返回 0", "0", String.valueOf(facade.dealInHospPublicity("7"))); check("关闭时药品宣教返回 0", "0", String.valueOf(facade.dealMedicalPublicity("7"))); check("关闭时不再调用内部实现(stub 调用次数 = 0)", "0", String.valueOf(StubApi.CALLS.get())); } System.out.println(); System.out.println("=== 场景 4:参数表【没有】publicity.enabled 这一行时,回落 application.yml ==="); try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_MISSING)) { check("参数行缺失、yml 默认 true 时 isEnabled() 为 true", "true", String.valueOf(ctx.getBean(PublicityFacade.class).isEnabled())); } try (AnnotationConfigApplicationContext ctx = context(false, true, SYS_MISSING)) { check("参数行缺失、yml=false 时 isEnabled() 为 false", "false", String.valueOf(ctx.getBean(PublicityFacade.class).isEnabled())); } System.out.println(); System.out.println("=== 场景 5:容器里【没有】IConfigService 时(独立/测试环境)必须能启动且按 yml 兜底 ==="); try (AnnotationConfigApplicationContext ctx = context(true, true, null, false)) { PublicityFacade facade = ctx.getBean(PublicityFacade.class); check("无配置服务时门面仍能装配并启动", "true", String.valueOf(facade != null)); check("无配置服务时按 yml 兜底(true)", "true", String.valueOf(facade.isEnabled())); } System.out.println(); System.out.println("=== 场景 6:controller + 开关拦截器 + WebConfig(第 16 步新增)==="); try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_OFF)) { // 受保护路径:只有两个"会生成宣教"的端点,/status 刻意不挂 String[] guarded = com.ruoyi.publicity.config.PublicityWebConfig.guardedPathPatterns(); check("受保护路径 2 条(两个生成端点)", "2", String.valueOf(guarded.length)); boolean hasIn = false, hasMed = false, hasStatus = false; for (String p : guarded) { if (com.ruoyi.publicity.config.PublicityWebConfig.DEAL_IN_HOSP_PUBLICITY_ENDPOINT.equals(p)) hasIn = true; if (com.ruoyi.publicity.config.PublicityWebConfig.DEAL_MEDICAL_PUBLICITY_ENDPOINT.equals(p)) hasMed = true; if (com.ruoyi.publicity.config.PublicityWebConfig.STATUS_ENDPOINT_PENDING.equals(p)) hasStatus = true; } check("含 /dealInHospPublicity", "true", String.valueOf(hasIn)); check("含 /dealMedicalPublicity", "true", String.valueOf(hasMed)); check("不含 /status(只读诊断入口必须一直可用)", "false", String.valueOf(hasStatus)); // 关闭时拦截器必须抛明确业务异常(而不是静默放行) com.ruoyi.publicity.config.PublicityAvailabilityInterceptor it = ctx.getBean(com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class); String thrown = "(未抛异常)"; try { it.preHandle(null, null, null); } catch (Throwable t) { thrown = t.getClass().getSimpleName(); } check("关闭时拦截器抛 ServiceException", "ServiceException", thrown); check("关闭时错误提示含键名 publicity.enabled", "true", String.valueOf( com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.DISABLED_MESSAGE .contains("publicity.enabled"))); check("关闭时错误提示点明与 in.helibrary.switch 是两回事", "true", String.valueOf( com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.DISABLED_MESSAGE .contains("in.helibrary.switch"))); } // 开启时必须放行 try (AnnotationConfigApplicationContext ctx = context(true, true, SYS_ON)) { com.ruoyi.publicity.config.PublicityAvailabilityInterceptor it = ctx.getBean(com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class); check("开启时拦截器放行", "true", String.valueOf(it.preHandle(null, null, null))); } System.out.println(); if (failed == 0) { System.out.println("===== 宣教模块 Spring 装配校验全部通过 ====="); } else { System.out.println("===== 宣教模块 Spring 装配校验失败 " + failed + " 项 ====="); System.exit(1); } } private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withImpl, String sysConfigValue) { return context(ymlEnabled, withImpl, sysConfigValue, true); } private static AnnotationConfigApplicationContext context(boolean ymlEnabled, boolean withImpl, String sysConfigValue, boolean withConfigService) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); PublicityProperties properties = new PublicityProperties(); properties.setEnabled(ymlEnabled); ctx.registerBean(PublicityProperties.class, () -> properties); ctx.register(PublicityFacade.class, PublicitySwitch.class, com.ruoyi.publicity.config.PublicityAvailabilityInterceptor.class, com.ruoyi.publicity.config.PublicityWebConfig.class); if (withConfigService) { // 假的参数配置服务:只认 publicity.enabled 这一个键,值由 sysConfigValue 决定 ctx.registerBean("stubConfigService", IConfigService.class, () -> (IConfigService) configKey -> PublicitySwitch.CONFIG_KEY.equals(configKey) ? sysConfigValue : ""); } if (withImpl) { ctx.registerBean("stubPublicityApi", PublicityApi.class, StubApi::new); } ctx.refresh(); return ctx; } /** * 顶替 {@code DefaultPublicityApi} 的桩:只回填调用次数与固定返回值,不碰数据库 */ public static class StubApi implements PublicityApi { /** 调用次数:用来证明"开关关闭时门面确实没有转发给实现" */ static final AtomicInteger CALLS = new AtomicInteger(); @Override public int dealInHospPublicity(String config) { CALLS.incrementAndGet(); return 1; } @Override public int dealMedicalPublicity(String config) { CALLS.incrementAndGet(); return 1; } } }