|
@@ -0,0 +1,333 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.HealthReportController;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthIndicator;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthReport;
|
|
|
|
|
+import com.etotem.cfc.service.HealthReportService;
|
|
|
|
|
+import com.etotem.cfc.service.PurchaseService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
|
|
+import org.mockito.Mock;
|
|
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyList;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测:创建健康报告(能力键 {@code health.report.create},TDD-RED 派发 S-cfc-145)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约锚点</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:{@code docs/tdd/SRS.md} «三、未覆盖控制器» —— {@code HealthReportController}(59 方法,0 覆盖);</li>
|
|
|
|
|
+ * <li>设计:{@code HealthReportController#createReport}(类级 {@code /api/health} + {@code @PostMapping("/report/create")});</li>
|
|
|
|
|
+ * <li>契约:{@code docs/superpowers/api/API_REFERENCE.md} §4.59.2「POST /api/health/report/create」:
|
|
|
|
|
+ * body {@code {memberId?, reportType, overallScore?, gutHealthScore?, chronicDiseaseScore?, nutritionScore?, gutAge?, gutType?, ...}}
|
|
|
|
|
+ * (reportType 必填:{@code physical_exam}/{@code gut_flora});响应 {@code HealthReport}(含 id);
|
|
|
|
|
+ * 创建健康报告(含指标明细);付费墙:未购买「健康启航计划」返回 {@code code=4001}「请先购买健康启航计划体验包」;
|
|
|
|
|
+ * memberId 缺省取 {@code currentMemberId}。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>测试范围</b>:仅 Mockito + 反射直接调用控制器方法(纯单测,无 Spring / 无 DB / 无外部服务),
|
|
|
|
|
+ * 与同批 RED 契约测试(TeamProfitHook / TeamStarMultiplier / TeamProfitPipeline / FamilyJoinValidationRules)风格一致。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>RED 预期</b>:契约将 reportType 枚举为且仅枚举 {@code physical_exam}/{@code gut_flora} 两值;
|
|
|
|
|
+ * 而 {@code HealthReportController#createReport} 当前仅判 {@code null}、未校验取值合法集,
|
|
|
|
|
+ * 空串/其他字符串会被放行到 service 层并返回 200。因此「reportType 空串/非法值须拒绝」两个契约用例预期失败(红),
|
|
|
|
|
+ * GREEN 阶段需在控制器补 reportType 取值校验使红例转绿。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>实现现状提示(编排侧转登记用)</b>:该键所映射端点 {@code createReport} 实现已落地(src/main,
|
|
|
|
|
+ * 且端点同源的 {@code health.report.createReport} 键已 GREEN 并复用登记于 API_REFERENCE §4.63)。
|
|
|
|
|
+ * 本批契约测试中除 reportType 取值校验外的契约行为(付费墙 4001 / 家庭 5001 / memberId 缺省取 currentMemberId /
|
|
|
|
|
+ * reportType 必填 / 指标明细透传)均已实现,预期绿;编排侧应复核密钥登记,避免与 createReport 键重复建池。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class HealthReportCreateContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String TARGET_PATH = "/api/health/report/create";
|
|
|
|
|
+
|
|
|
|
|
+ private static final long USER_ID = 1L;
|
|
|
|
|
+ private static final long FAMILY_ID = 7L;
|
|
|
|
|
+ private static final long CURRENT_MEMBER_ID = 6L;
|
|
|
|
|
+
|
|
|
|
|
+ private static final String MSG_PURCHASE_REQUIRED = "请先购买健康启航计划体验包";
|
|
|
|
|
+ private static final String MSG_NO_FAMILY = "请先创建或加入家庭";
|
|
|
|
|
+ private static final String MSG_MEMBER_REQUIRED = "memberId不能为空";
|
|
|
|
|
+ private static final String MSG_REPORT_TYPE_REQUIRED = "reportType不能为空: physical_exam / gut_flora";
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private HealthReportService healthReportService;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private PurchaseService purchaseService;
|
|
|
|
|
+
|
|
|
|
|
+ private HealthReportController controller;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ controller = new HealthReportController();
|
|
|
|
|
+ // 仅注入 createReport 使用的两个依赖;其余 @Resource 字段保持 null(反射注入,与同批 REF 风格一致)
|
|
|
|
|
+ injectField(controller, "healthReportService", healthReportService);
|
|
|
|
|
+ injectField(controller, "purchaseService", purchaseService);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // 正常流:契约 §4.59.2
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-1:已购计划 + 合法参数 → 返回创建的报告(含 id),service 落库一次")
|
|
|
|
|
+ void createReport_success_returnsCreatedReportWithId() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+ HealthReport created = new HealthReport();
|
|
|
|
|
+ created.setId(100L);
|
|
|
|
|
+ when(healthReportService.createReport(any(HealthReport.class), anyList())).thenReturn(created);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = baseParams(5L, "physical_exam");
|
|
|
|
|
+ params.put("overallScore", 88);
|
|
|
|
|
+ params.put("indicators", new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(params, USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
|
|
+ assertNotNull(result.getData(), "[health.report.create] 正常创建应返回 HealthReport(含 id)");
|
|
|
|
|
+ assertEquals(100L, result.getData().getId().longValue());
|
|
|
|
|
+ verify(healthReportService).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-2:memberId 缺省 → 缺省取 currentMemberId(契约显式约定)")
|
|
|
|
|
+ void createReport_memberIdDefaultsToCurrentMemberId() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+ when(healthReportService.createReport(any(HealthReport.class), any())).thenReturn(new HealthReport());
|
|
|
|
|
+
|
|
|
|
|
+ // params 不含 memberId,仅依赖 currentMemberId
|
|
|
|
|
+ Map<String, Object> params = baseParams(null, "physical_exam");
|
|
|
|
|
+ params.put("indicators", new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ controller.createReport(params, USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<HealthReport> captor = ArgumentCaptor.forClass(HealthReport.class);
|
|
|
|
|
+ verify(healthReportService).createReport(captor.capture(), any());
|
|
|
|
|
+ assertEquals(CURRENT_MEMBER_ID, captor.getValue().getUserId(),
|
|
|
|
|
+ "[health.report.create] memberId 缺省时应取 currentMemberId(契约 §4.59.2)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-3:指标明细 → 原样透传 service(保序 + 字段对齐)")
|
|
|
|
|
+ void createReport_indicatorsPassedThroughInOrder() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+ when(healthReportService.createReport(any(HealthReport.class), anyList())).thenReturn(new HealthReport());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = baseParams(5L, "gut_flora");
|
|
|
|
|
+ List<Map<String, Object>> indicators = new ArrayList<>();
|
|
|
|
|
+ Map<String, Object> wbc = new HashMap<>();
|
|
|
|
|
+ wbc.put("category", "blood");
|
|
|
|
|
+ wbc.put("indicatorName", "WBC");
|
|
|
|
|
+ wbc.put("indicatorValue", "6.1");
|
|
|
|
|
+ wbc.put("unit", "10^9/L");
|
|
|
|
|
+ wbc.put("status", "normal");
|
|
|
|
|
+ Map<String, Object> alt = new HashMap<>();
|
|
|
|
|
+ alt.put("category", "liver");
|
|
|
|
|
+ alt.put("indicatorName", "ALT");
|
|
|
|
|
+ alt.put("indicatorValue", "32");
|
|
|
|
|
+ alt.put("unit", "U/L");
|
|
|
|
|
+ indicators.add(wbc);
|
|
|
|
|
+ indicators.add(alt);
|
|
|
|
|
+ params.put("indicators", indicators);
|
|
|
|
|
+
|
|
|
|
|
+ controller.createReport(params, USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
|
|
|
|
|
+ verify(healthReportService).createReport(any(HealthReport.class), captor.capture());
|
|
|
|
|
+ List<HealthIndicator> passed = captor.getValue();
|
|
|
|
|
+ assertNotNull(passed);
|
|
|
|
|
+ assertEquals(2, passed.size());
|
|
|
|
|
+ assertEquals("WBC", passed.get(0).getIndicatorName());
|
|
|
|
|
+ assertEquals(0, passed.get(0).getSortOrder().intValue());
|
|
|
|
|
+ assertEquals("ALT", passed.get(1).getIndicatorName());
|
|
|
|
|
+ assertEquals(1, passed.get(1).getSortOrder().intValue());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // 边界流:付费墙 / 家庭 / 必填参数 / reportType 取值校验
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-1:未购计划 → code=4001「请先购买健康启航计划体验包」,service 不调用")
|
|
|
|
|
+ void createReport_notPurchased_rejectsWith4001() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(false));
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ baseParams(5L, "physical_exam"), USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(4001, result.getCode(),
|
|
|
|
|
+ "[health.report.create] 未购买健康启航计划应返回 code=4001(付费墙契约 §4.59.2)");
|
|
|
|
|
+ assertEquals(MSG_PURCHASE_REQUIRED, result.getMessage());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-2:无家庭 → code=5001「请先创建或加入家庭」")
|
|
|
|
|
+ void createReport_noFamily_rejected() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ baseParams(5L, "physical_exam"), USER_ID, null, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(5001, result.getCode());
|
|
|
|
|
+ assertEquals(MSG_NO_FAMILY, result.getMessage());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-3:memberId 与 currentMemberId 均缺 → 拒绝「memberId不能为空」")
|
|
|
|
|
+ void createReport_memberIdMissing_rejected() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ baseParams(null, "physical_exam"), USER_ID, FAMILY_ID, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
|
|
+ assertEquals(MSG_MEMBER_REQUIRED, result.getMessage());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-4:reportType 缺省 null → 拒绝「reportType不能为空: physical_exam / gut_flora」")
|
|
|
|
|
+ void createReport_reportTypeNull_rejected() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ baseParams(5L, null), USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
|
|
+ assertEquals(MSG_REPORT_TYPE_REQUIRED, result.getMessage());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("RED-边界流-5:reportType 空串 → 契约仅允许 physical_exam/gut_flora 两值,应拒绝")
|
|
|
|
|
+ void createReport_reportTypeEmptyString_rejected() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+
|
|
|
|
|
+ // params.put("reportType", "") 等价于传空串
|
|
|
|
|
+ Map<String, Object> params = baseParams(5L, "");
|
|
|
|
|
+ params.put("indicators", new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ params, USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ // 契约:reportType 必填且仅枚举 physical_exam / gut_flora 两值 —— 空串并非合法取值,应与 null
|
|
|
|
|
+ // 同口径拒绝而非放行到 service。当前实现仅判 null(空串放行返回 200),本用例应红(RED)。
|
|
|
|
|
+ assertTrue(result.getCode() != 200,
|
|
|
|
|
+ "[health.report.create] RED:reportType=空串 应被拒绝(契约仅允许 physical_exam/gut_flora);"
|
|
|
|
|
+ + "当前实现仅判 null 未校验合法值,实际放行 code=" + result.getCode());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("RED-边界流-6:reportType 非法值 → 契约仅允许 physical_exam/gut_flora 两值,应拒绝")
|
|
|
|
|
+ void createReport_reportTypeInvalidValue_rejected() {
|
|
|
|
|
+ when(purchaseService.checkPurchase(USER_ID)).thenReturn(purchaseCheck(true));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> params = baseParams(5L, "dna");
|
|
|
|
|
+ params.put("indicators", new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ Result<HealthReport> result = controller.createReport(
|
|
|
|
|
+ params, USER_ID, FAMILY_ID, CURRENT_MEMBER_ID);
|
|
|
|
|
+
|
|
|
|
|
+ // 契约仅枚举两个合法值:physical_exam / gut_flora;"dna" 属契约外值,应拒绝。
|
|
|
|
|
+ // 当前实现未做取值校验(放行返回 200),本用例应红(RED)。
|
|
|
|
|
+ assertTrue(result.getCode() != 200,
|
|
|
|
|
+ "[health.report.create] RED:reportType=dna 应被拒绝(契约仅允许 physical_exam/gut_flora);"
|
|
|
|
|
+ + "当前实现未校验合法值,实际放行 code=" + result.getCode());
|
|
|
|
|
+ verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // 权限流:JwtInterceptor 拦截(/api/** 8 个公开路径之外)
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流-1:/api/health/report/create 非公开白名单 → 必须登录(LOGIN_REQUIRED_PATHS 含报告族)")
|
|
|
|
|
+ void createReport_loginRequired_notInPublicWhiteList() throws Exception {
|
|
|
|
|
+ String[] publicPaths = (String[]) readPrivateStatic(
|
|
|
|
|
+ "com.etotem.cfc.config.JwtInterceptor", "PUBLIC_PATHS");
|
|
|
|
|
+ String[] loginRequiredPaths = (String[]) readPrivateStatic(
|
|
|
|
|
+ "com.etotem.cfc.config.JwtInterceptor", "LOGIN_REQUIRED_PATHS");
|
|
|
|
|
+
|
|
|
|
|
+ boolean coveredByPublic = false;
|
|
|
|
|
+ for (String publicPath : publicPaths) {
|
|
|
|
|
+ String boundary = publicPath.endsWith("/**")
|
|
|
|
|
+ ? publicPath.substring(0, publicPath.length() - 3) : publicPath;
|
|
|
|
|
+ if (TARGET_PATH.equals(publicPath) || TARGET_PATH.startsWith(boundary + "/")) {
|
|
|
|
|
+ coveredByPublic = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertFalse(coveredByPublic,
|
|
|
|
|
+ "[health.report.create] 创建报告属家庭私有健康数据,PUBLIC_PATHS 不应覆盖 " + TARGET_PATH);
|
|
|
|
|
+
|
|
|
|
|
+ boolean loginRequiredRegistered = false;
|
|
|
|
|
+ for (String p : loginRequiredPaths) {
|
|
|
|
|
+ if ("/api/health/report/".equals(p)) {
|
|
|
|
|
+ loginRequiredRegistered = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ assertTrue(loginRequiredRegistered,
|
|
|
|
|
+ "[health.report.create] JwtInterceptor LOGIN_REQUIRED_PATHS 应含 /api/health/report/"
|
|
|
|
|
+ + "(报告族登录必达,无 token 直接 401)");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // 辅助
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> purchaseCheck(boolean active) {
|
|
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
|
|
+ m.put("isActive", active);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> baseParams(Object memberId, Object reportType) {
|
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
|
+ if (memberId != null) {
|
|
|
|
|
+ params.put("memberId", memberId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (reportType != null) {
|
|
|
|
|
+ params.put("reportType", reportType);
|
|
|
|
|
+ }
|
|
|
|
|
+ return params;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void injectField(Object target, String fieldName, Object value) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(fieldName);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object readPrivateStatic(String className, String fieldName) throws Exception {
|
|
|
|
|
+ Class<?> clazz = Class.forName(className);
|
|
|
|
|
+ Field f = clazz.getDeclaredField(fieldName);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ return f.get(null);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|