|
@@ -0,0 +1,324 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.DataMigrationController;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.MigrationController;
|
|
|
|
|
+import com.etotem.cfc.controller.admin.ReportParseAdminController;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthReportDraft;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthReportDraftMapper;
|
|
|
|
|
+import com.etotem.cfc.service.DataMigrationService;
|
|
|
|
|
+import com.etotem.cfc.service.HealthReportDraftService;
|
|
|
|
|
+import com.etotem.cfc.service.ReportCollectService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.annotation.Annotation;
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.lang.reflect.Method;
|
|
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
|
|
+import java.lang.reflect.Parameter;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
|
|
|
+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.anyLong;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
|
|
+import static org.mockito.Mockito.doThrow;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约级单测:能力键 {@code admin.data.migration}(S-cfc-205,TDD-RED 阶段)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约溯源</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-10(数据迁移 / 参数校验:
|
|
|
|
|
+ * 跑迁移脚本 → 校验参数 → 确认无孤儿数据;边界:迁移幂等;字段校验失败阻断);</li>
|
|
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.60.10(
|
|
|
|
|
+ * {@code POST /api/migration/run}、{@code POST /api/migrate/guide-packages}、
|
|
|
|
|
+ * {@code /migrate/knowledge-base-to-articles}、{@code /api/admin/report-parse/backfill-food}、
|
|
|
|
|
+ * {@code /retry});</li>
|
|
|
|
|
+ * <li>用例:{@code tests/test-cases/admin.md} TC-A-15 / TC-A-15-E / TC-A-15-P。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约范围</b>(3 个迁移/参数校验入口的统一契约)</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>{@code DataMigrationController}(/api/migration/run)——手动触发数据迁移:全部成功返回成功码;
|
|
|
|
|
+ * 部分失败必须返回<b>明确错误码</b>,不得以成功码伪装完成;重复执行<b>幂等</b>(不得再次触发迁移);</li>
|
|
|
|
|
+ * <li>{@code MigrationController}(/api/migrate/guide-packages、/knowledge-base-to-articles)——
|
|
|
|
|
+ * 专项迁移:DDL/源库故障必须返回<b>明确错误结果</b>,不得吞异常返回成功、不得向调用方抛出原始异常;</li>
|
|
|
|
|
+ * <li>{@code ReportParseAdminController}(/api/admin/report-parse/backfill-food、/retry、/list)——
|
|
|
|
|
+ * 参数校验:{@code draftId}/{@code reportId} 非正数、{@code page} 非正数必须<b>阻断并返回明确错误</b>
|
|
|
|
|
+ * (字段校验失败阻断)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>覆盖流</b>(纯 Mockito,无 Spring 上下文 / 无 DB / 无外部服务):正常流 / 异常流·边界流(明确错误)/
|
|
|
|
|
+ * 幂等流 / 权限流(TC-A-15-P:非 admin 角色访问管理端数据迁移接口 → 401/403)。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>RED 预期</b>(当前 src/main 现状,逐项核实):</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>{@code DataMigrationController.runMigration()} 部分失败走 {@code else} 分支返回
|
|
|
|
|
+ * {@code Result.success("迁移过程中有部分错误", ...)}(code=200)——未返回错误码;且无幂等守卫,
|
|
|
|
|
+ * 每次调用都触发 {@code dataMigrationService.runMigration()};</li>
|
|
|
|
|
+ * <li>{@code MigrationController.migrateGuidePackages()} 用空 {@code catch (Exception ignored)}
|
|
|
|
|
+ * 吞掉 DDL 异常后仍返回 {@code Result.success("迁移完成")};{@code migrateKnowledgeBaseToArticles()}
|
|
|
|
|
+ * 源库故障时异常直接上抛(无 try/catch 转换);</li>
|
|
|
|
|
+ * <li>{@code ReportParseAdminController} 的 {@code retry}/{@code backfillFood} 仅判 {@code null},
|
|
|
|
|
+ * 不拒绝 {@code draftId<=0}/{@code reportId<=0};{@code list} 对 {@code page<1} 无校验。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ * 因此本类契约断言在实现补齐前应<b>失败(红)</b>;GREEN 阶段补齐上述行为后转绿。
|
|
|
|
|
+ */
|
|
|
|
|
+class AdminDataMigrationContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String KEY = "[admin.data.migration]";
|
|
|
|
|
+
|
|
|
|
|
+ private DataMigrationController dataMigrationController;
|
|
|
|
|
+ private MigrationController migrationController;
|
|
|
|
|
+ private ReportParseAdminController reportParseAdminController;
|
|
|
|
|
+
|
|
|
|
|
+ private DataMigrationService dataMigrationService;
|
|
|
|
|
+ private JdbcTemplate jdbcTemplate;
|
|
|
|
|
+ private HealthReportDraftMapper draftMapper;
|
|
|
|
|
+ private HealthReportDraftService healthReportDraftService;
|
|
|
|
|
+ private ReportCollectService reportCollectService;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ dataMigrationController = new DataMigrationController();
|
|
|
|
|
+ injectAll(dataMigrationController);
|
|
|
|
|
+ dataMigrationService = (DataMigrationService) readField(dataMigrationController, "dataMigrationService");
|
|
|
|
|
+
|
|
|
|
|
+ migrationController = new MigrationController();
|
|
|
|
|
+ injectAll(migrationController);
|
|
|
|
|
+ jdbcTemplate = (JdbcTemplate) readField(migrationController, "jdbcTemplate");
|
|
|
|
|
+
|
|
|
|
|
+ reportParseAdminController = new ReportParseAdminController();
|
|
|
|
|
+ injectAll(reportParseAdminController);
|
|
|
|
|
+ draftMapper = (HealthReportDraftMapper) readField(reportParseAdminController, "draftMapper");
|
|
|
|
|
+ healthReportDraftService = (HealthReportDraftService) readField(reportParseAdminController, "healthReportDraftService");
|
|
|
|
|
+ reportCollectService = (ReportCollectService) readField(reportParseAdminController, "reportCollectService");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常流(契约基线:已符合,记录 GREEN 目标) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-1:三个迁移/参数校验契约类应存在")
|
|
|
|
|
+ void 正常流_契约类存在() {
|
|
|
|
|
+ assertNotNull(loadClass("com.etotem.cfc.controller.admin.DataMigrationController"),
|
|
|
|
|
+ KEY + " DataMigrationController(/api/migration/run)应存在(§4.60.10)。");
|
|
|
|
|
+ assertNotNull(loadClass("com.etotem.cfc.controller.admin.MigrationController"),
|
|
|
|
|
+ KEY + " MigrationController(/api/migrate/*)应存在(§4.60.10)。");
|
|
|
|
|
+ assertNotNull(loadClass("com.etotem.cfc.controller.admin.ReportParseAdminController"),
|
|
|
|
|
+ KEY + " ReportParseAdminController(/api/admin/report-parse/*)应存在(§4.60.10)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-2:runMigration 全部成功应返回成功码")
|
|
|
|
|
+ void 正常流_runMigration全部成功_返回成功码() {
|
|
|
|
|
+ Map<String, Object> ok = new HashMap<>();
|
|
|
|
|
+ ok.put("success", true);
|
|
|
|
|
+ when(dataMigrationService.runMigration()).thenReturn(ok);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = dataMigrationController.runMigration();
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " 迁移全部成功应返回成功码(TC-A-15 正常流)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 异常流:明确错误(非伪成功) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-1:runMigration 部分失败必须返回错误码,不得以成功码伪装完成")
|
|
|
|
|
+ void 异常流_runMigration部分失败_必须返回错误码() {
|
|
|
|
|
+ Map<String, Object> partial = new HashMap<>();
|
|
|
|
|
+ partial.put("success", false);
|
|
|
|
|
+ partial.put("errors", Collections.singletonList("迁移中断: 源连接超时"));
|
|
|
|
|
+ when(dataMigrationService.runMigration()).thenReturn(partial);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = dataMigrationController.runMigration();
|
|
|
|
|
+
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " 迁移部分失败必须返回错误码(Result.error),不得以成功码伪装完成"
|
|
|
|
|
+ + "(TC-A-15-E:中断回滚 + 明确错误非500)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-2:guide-packages 专项迁移 DDL 失败必须返回错误码,不得吞异常伪装成功")
|
|
|
|
|
+ void 异常流_guidePackages迁移_DDL失败返回错误码() {
|
|
|
|
|
+ doThrow(new RuntimeException("模拟 guide_packages 表不存在/DDL 失败"))
|
|
|
|
|
+ .when(jdbcTemplate).execute(anyString());
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> result = migrationController.migrateGuidePackages();
|
|
|
|
|
+
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " guide-packages 专项迁移 DDL 失败不得吞异常伪装成功(SC-A-10 字段校验失败阻断;"
|
|
|
|
|
+ + "TC-A-15-E 明确错误非500)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-3:知识库迁移源库故障应返回明确错误结果,不得向调用方抛出原始异常")
|
|
|
|
|
+ void 异常流_知识库迁移_源库故障返回明确错误结果() {
|
|
|
|
|
+ when(jdbcTemplate.queryForList(anyString()))
|
|
|
|
|
+ .thenThrow(new RuntimeException("源库连接中断"));
|
|
|
|
|
+
|
|
|
|
|
+ assertDoesNotThrow(() -> {
|
|
|
|
|
+ Result<String> result = migrationController.migrateKnowledgeBaseToArticles();
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " 知识库迁移源库故障必须返回明确错误码(TC-A-15-E:明确错误非500)。");
|
|
|
|
|
+ }, KEY + " 知识库迁移源库故障不得向调用方抛出原始异常,应转为明确错误结果(TC-A-15-E)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 边界流:字段校验失败阻断(SC-A-10 边界) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-1:report-parse/retry draftId 非正数应阻断并返回明确错误")
|
|
|
|
|
+ void 边界流_retry_draftId非正数_阻断() {
|
|
|
|
|
+ Result<Map<String, Object>> result = reportParseAdminController.retry(params("draftId", 0L));
|
|
|
|
|
+
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " report-parse/retry 的 draftId 非法(<=0) 必须阻断并返回明确错误"
|
|
|
|
|
+ + "(SC-A-10 字段校验失败阻断)。");
|
|
|
|
|
+ verify(reportCollectService, never()).submitAsyncCollect(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-2:report-parse/backfill-food reportId 非正数应阻断并返回明确错误")
|
|
|
|
|
+ void 边界流_backfillFood_reportId非正数_阻断() {
|
|
|
|
|
+ Result<Map<String, Object>> result = reportParseAdminController.backfillFood(params("reportId", 0L));
|
|
|
|
|
+
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " report-parse/backfill-food 的 reportId 非法(<=0) 必须阻断并返回明确错误"
|
|
|
|
|
+ + "(SC-A-10 字段校验失败阻断)。");
|
|
|
|
|
+ verify(reportCollectService, never()).reparseReportFoods(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界流-3:report-parse/list page 非正数应阻断并返回明确错误")
|
|
|
|
|
+ void 边界流_list_page非正数_阻断() {
|
|
|
|
|
+ when(draftMapper.selectPage(any(), any())).thenReturn(new Page<HealthReportDraft>());
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = reportParseAdminController.list(params("page", 0, "size", 20));
|
|
|
|
|
+
|
|
|
|
|
+ assertNotEquals(Integer.valueOf(200), result.getCode(),
|
|
|
|
|
+ KEY + " report-parse/list 的 page<1 必须阻断并返回明确错误(SC-A-10 字段校验失败阻断)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 幂等流:重复执行幂等(SC-A-10 边界 / TC-A-15-E) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("幂等流:重复执行 runMigration 不得再次触发迁移(幂等/提示已迁移)")
|
|
|
|
|
+ void 幂等流_重复执行runMigration_不得再次触发() {
|
|
|
|
|
+ Map<String, Object> ok = new HashMap<>();
|
|
|
|
|
+ ok.put("success", true);
|
|
|
|
|
+ when(dataMigrationService.runMigration()).thenReturn(ok);
|
|
|
|
|
+
|
|
|
|
|
+ dataMigrationController.runMigration();
|
|
|
|
|
+ dataMigrationController.runMigration(); // 第二次:契约要求幂等,不得重复触发迁移
|
|
|
|
|
+
|
|
|
|
|
+ verify(dataMigrationService, times(1)).runMigration();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 权限流:非 admin 角色访问管理端数据迁移接口 → 401/403(TC-A-15-P) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("权限流:迁移/报告解析管理端接口应实施 admin 角色门禁(非 admin 401/403)")
|
|
|
|
|
+ void 权限流_管理端接口_应实施admin角色门禁() {
|
|
|
|
|
+ assertTrue(hasRoleGate(method(DataMigrationController.class, "runMigration")),
|
|
|
|
|
+ KEY + " POST /api/migration/run 应实施 admin 角色门禁(非 admin 401/403,TC-A-15-P)。");
|
|
|
|
|
+ assertTrue(hasRoleGate(method(MigrationController.class, "migrateGuidePackages")),
|
|
|
|
|
+ KEY + " POST /api/migrate/guide-packages 应实施 admin 角色门禁(TC-A-15-P)。");
|
|
|
|
|
+ assertTrue(hasRoleGate(method(MigrationController.class, "migrateKnowledgeBaseToArticles")),
|
|
|
|
|
+ KEY + " POST /api/migrate/knowledge-base-to-articles 应实施 admin 角色门禁(TC-A-15-P)。");
|
|
|
|
|
+ assertTrue(hasRoleGate(method(ReportParseAdminController.class, "list", Map.class)),
|
|
|
|
|
+ KEY + " POST /api/admin/report-parse/list 应实施 admin 角色门禁(TC-A-15-P)。");
|
|
|
|
|
+ assertTrue(hasRoleGate(method(ReportParseAdminController.class, "retry", Map.class)),
|
|
|
|
|
+ KEY + " POST /api/admin/report-parse/retry 应实施 admin 角色门禁(TC-A-15-P)。");
|
|
|
|
|
+ assertTrue(hasRoleGate(method(ReportParseAdminController.class, "backfillFood", Map.class)),
|
|
|
|
|
+ KEY + " POST /api/admin/report-parse/backfill-food 应实施 admin 角色门禁(TC-A-15-P)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 反射/Mockito 辅助 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static Object readField(Object target, String name) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ return f.get(target);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 对目标对象所有非 static/final 字段注入 Mockito mock(仿 ReportParserAdminContractTest 既有模式) */
|
|
|
|
|
+ private static void injectAll(Object target) throws Exception {
|
|
|
|
|
+ for (Field f : target.getClass().getDeclaredFields()) {
|
|
|
|
|
+ if (Modifier.isStatic(f.getModifiers()) || Modifier.isFinal(f.getModifiers())) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, mock(f.getType()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Method method(Class<?> clazz, String name, Class<?>... paramTypes) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return clazz.getDeclaredMethod(name, paramTypes);
|
|
|
|
|
+ } catch (NoSuchMethodException e) {
|
|
|
|
|
+ throw new IllegalStateException(KEY + " 契约方法缺失: " + clazz.getSimpleName() + "." + name, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 探测某 handler 是否实施 admin 角色门禁:方法带 role/request 属性参数,或方法/类带角色授权注解 */
|
|
|
|
|
+ private boolean hasRoleGate(Method m) {
|
|
|
|
|
+ for (Parameter p : m.getParameters()) {
|
|
|
|
|
+ if (p.getType().getSimpleName().equals("HttpServletRequest")) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("role".equals(p.getName()) || "currentRole".equals(p.getName())) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (Annotation a : p.getAnnotations()) {
|
|
|
|
|
+ String n = a.annotationType().getSimpleName();
|
|
|
|
|
+ if (n.contains("RequestAttribute") || n.toLowerCase().contains("role")) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return hasRoleSemanticAnnotation(m.getDeclaringClass().getAnnotations())
|
|
|
|
|
+ || hasRoleSemanticAnnotation(m.getAnnotations());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean hasRoleSemanticAnnotation(Annotation[] anns) {
|
|
|
|
|
+ for (Annotation a : anns) {
|
|
|
|
|
+ String n = a.annotationType().getSimpleName().toLowerCase();
|
|
|
|
|
+ if (n.contains("role") || n.contains("admin") || n.contains("require") || n.contains("authorize")) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> params(Object... kv) {
|
|
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
|
|
+ for (int i = 0; i + 1 < kv.length; i += 2) {
|
|
|
|
|
+ m.put((String) kv[i], kv[i + 1]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|