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;
|
|
/**
|
* 宣教模块的 <b>Spring 容器装配校验</b>(不依赖 JUnit、不依赖数据库)。
|
*
|
* <p>与 {@code InhospVisitContextCheck} 同一套做法:用真实的(但极简的)
|
* {@link AnnotationConfigApplicationContext} 验证"Spring 能把 Bean 装起来"。
|
* 断言覆盖满意度模块实际踩过的两个坑:
|
* <ol>
|
* <li>门面有两个公开构造器却没标 {@code @Autowired} → 启动报 {@code No default constructor found};</li>
|
* <li>门面把自己当实现 → 调用时无限递归 → {@code StackOverflowError}。</li>
|
* </ol>
|
*
|
* <h3>运行方式</h3>
|
* <pre>
|
* mvn -o -q -pl ruoyi-publicity -am test-compile -DskipTests
|
* # 然后用模块 classpath 直接跑 main(surefire 认不出它,因为没有 @Test)
|
* </pre>
|
*/
|
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;
|
}
|
}
|
}
|