|
|
@@ -0,0 +1,381 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.lang.annotation.Annotation;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.net.JarURLConnection;
|
|
|
+import java.net.URL;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Enumeration;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.jar.JarFile;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测(TDD-RED,S-cfc-308):能力键 {@code admin.familyRelation.manage}
|
|
|
+ * (平台运营·关系类型 CRUD 管理,单元 admin)。
|
|
|
+ *
|
|
|
+ * <p><b>需求溯源</b>:{@code tests/user-scenarios/应用场景总览.md} <b>SC-A-04 关系类型 CRUD</b>——
|
|
|
+ * 目标:管理家庭关系;流程:关系类型增删改查 → 生效;边界:被引用时删需确认;模块:{@code RelationshipTypes.vue}。</p>
|
|
|
+ *
|
|
|
+ * <p><b>用例绑定(TC 台账 → 资产回溯锚点)</b>:{@code tests/test-cases/admin.md}
|
|
|
+ * <ul>
|
|
|
+ * <li><b>TC-A-10</b>(功能):新增/编辑/停用关系类型 → 操作成功;家长端关系选择器同步生效;</li>
|
|
|
+ * <li><b>TC-A-10-E</b>(异常):新增重名拒绝;停用被引用类型提示先解绑;名称含注入 400/转义;</li>
|
|
|
+ * <li><b>TC-A-10-P</b>(权限):非 admin 角色访问管理端接口统一 401/403,不泄露后台数据。</li>
|
|
|
+ * </ul></p>
|
|
|
+ *
|
|
|
+ * <p><b>设计溯源</b>:{@code docs/superpowers/api/API_REFERENCE.md}
|
|
|
+ * <ul>
|
|
|
+ * <li>§4.2 / §4.20:家庭域 {@code /api/family/relationship/*}({@code RelationshipQualityController})
|
|
|
+ * 仅提供<b>只读评分查询</b>(scores/calculate/health-alerts/milestones/pairwise),无 CRUD 载体;</li>
|
|
|
+ * <li>§4.60.18 契约缺口:SC-A-04 登记「无端点载体,需产品侧确认」——{@code relationship_types} 表已废弃 DROP,
|
|
|
+ * 关系由 {@code family_relationships}(generation 辈分模型 + {@code FamilyRelationshipService})承载;</li>
|
|
|
+ * <li><b>本键 design 需求(契约目标)</b>:新增 admin/family-relation 端点
|
|
|
+ * GET/POST/PUT/DELETE {@code /api/admin/family-relation/types}(关系类型增删改查 + 字典管理),
|
|
|
+ * 模块 {@code AdminFamilyRelationController}(待建)。</li>
|
|
|
+ * </ul></p>
|
|
|
+ *
|
|
|
+ * <p><b>实现现状(src/main 核实)</b>:{@code /api/admin/family-relation} 路径全库 0 引用,
|
|
|
+ * {@code AdminFamilyRelationController} 不存在,任何关系类型字典 CRUD 操作均未落地 → 本契约应全红。
|
|
|
+ * (若实测意外全绿,说明实现已存在,按编排侧流程转测试登记,不私改实现。)</p>
|
|
|
+ *
|
|
|
+ * <p><b>包路径不定约定</b>:O-2 方案 A 实现待定,<b>不得假定宿主包路径</b>。故本测试不硬编码
|
|
|
+ * {@code AdminFamilyRelationController} 的 FQCN,改为<b>类路径扫描</b> {@code com.etotem.cfc.controller}
|
|
|
+ * 下映射 {@code /api/admin/family-relation} 的控制器(含子包递归)——GREEN 实现放任意子包均可被探测。
|
|
|
+ * 若类路径扫描失败也会显式红(扫描器自检用例),避免「扫描器坏 → 永久红/假绿」。</p>
|
|
|
+ *
|
|
|
+ * <p><b>动词约定</b>:项目接口统一 {@code @PostMapping},而本键 design 明确要求 GET/POST/PUT/DELETE 动词端点。
|
|
|
+ * 测试按<b>语义操作</b>(列表/新增/编辑/停用)探测,不强制 HTTP 动词——GREEN 无论按 REST 动词还是
|
|
|
+ * POST 子路径({@code /types/list} 等)落地均可转绿;设计动词要求记录于下方。</p>
|
|
|
+ *
|
|
|
+ * <p><b>契约方法签名(实现方据此落地,GREEN 后本测试自动转绿)</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>控制器类级映射 {@code /api/admin/family-relation}(包路径任意,扫描发现)</li>
|
|
|
+ * <li>列表:{@code GET|POST <prefix>/types} 或 {@code /types/list} —— 关系类型字典列表(TC-A-10 查询)</li>
|
|
|
+ * <li>新增:{@code POST <prefix>/types} 或 {@code /types/save} —— 新增(重名校验,TC-A-10-E)</li>
|
|
|
+ * <li>编辑:{@code PUT|POST <prefix>/types} 或 {@code /types/update} —— 编辑(TC-A-10)</li>
|
|
|
+ * <li>停用/删除:{@code DELETE|POST <prefix>/types} 或 {@code /types/disable|delete} —— 停用;
|
|
|
+ * 被 {@code family_relationships} 引用时删需确认/提示先解绑(SC-A-04 边界 + TC-A-10-E)</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>测试风格</b>:与同批 RED 契约测试一致——纯反射 + 类路径扫描,无 Spring 上下文 / 无 DB / 无外部服务;
|
|
|
+ * 宿主缺失即全部用例红(RED),断言原文清晰可贴;GREEN 实现宿主后自动转绿。行为级细节(重名拒绝、
|
|
|
+ * 被引用确认、注入防护)由 GREEN 阶段行为级测试补充验证,本类负责「载体 + 操作是否存在」的契约锚定。</p>
|
|
|
+ */
|
|
|
+class AdminFamilyRelationManageContractTest {
|
|
|
+
|
|
|
+ /** 契约端点前缀(design 明确):关系类型字典管理 */
|
|
|
+ private static final String API_PREFIX = "/api/admin/family-relation";
|
|
|
+
|
|
|
+ /** 类型字典资源段(GET/POST/PUT/DELETE /api/admin/family-relation/types) */
|
|
|
+ private static final String TYPES = "types";
|
|
|
+
|
|
|
+ /** 类路径扫描根(项目控制器统一目录,含子包) */
|
|
|
+ private static final String CONTROLLER_BASE_PKG = "com.etotem.cfc.controller";
|
|
|
+
|
|
|
+ /** 扫描自检锚点:既有的只读评分控制器(证明扫描器有效,非扫描故障导致红) */
|
|
|
+ private static final String SANITY_EXISTING =
|
|
|
+ "com.etotem.cfc.controller.family.RelationshipQualityController";
|
|
|
+
|
|
|
+ // ==================== 正常流:/api/admin/family-relation 管理控制器载体 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:/api/admin/family-relation 管理控制器应存在(包路径不定,类路径扫描发现)")
|
|
|
+ void normalFlow_controllerCarrierExists() {
|
|
|
+ // 扫描器自检:既能发现既有控制器,又未命中 /api/admin/family-relation
|
|
|
+ assertTrue(scanControllerClasses().contains(SANITY_EXISTING),
|
|
|
+ "RED[admin.familyRelation.manage] 类路径扫描未发现既有控制器 " + SANITY_EXISTING
|
|
|
+ + ":扫描器故障会导致后续探测失真(本应为 GREEN 自检锚点)。");
|
|
|
+ assertFalse(discoverAdminFamilyRelationControllers().isEmpty(),
|
|
|
+ "RED[admin.familyRelation.manage] 未发现映射 " + API_PREFIX + " 的管理控制器"
|
|
|
+ + "(AdminFamilyRelationController 待建,包路径不定 → 类路径扫描):"
|
|
|
+ + "契约要求 GET/POST/PUT/DELETE " + API_PREFIX + "/types 承载关系类型字典"
|
|
|
+ + "增删改查 + 字典管理(SC-A-04 + TC-A-10)。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:应提供关系类型列表/查询接口(types · 字典列表,TC-A-10 查询)")
|
|
|
+ void normalFlow_typesListOperation() {
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.LIST),
|
|
|
+ "RED[admin.familyRelation.manage] 控制器缺少关系类型列表/查询操作"
|
|
|
+ + "(GET|POST " + API_PREFIX + "/types 或 /types/list):"
|
|
|
+ + "TC-A-10 关系类型管理页需展示字典列表。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:应提供关系类型新增接口(types · 新增,TC-A-10)")
|
|
|
+ void normalFlow_typesCreateOperation() {
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.CREATE),
|
|
|
+ "RED[admin.familyRelation.manage] 控制器缺少关系类型新增操作"
|
|
|
+ + "(POST " + API_PREFIX + "/types 或 /types/save):"
|
|
|
+ + "TC-A-10「新增关系类型 → 操作成功、家长端关系选择器同步生效」。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-4:应提供关系类型编辑接口(types · 编辑,TC-A-10)")
|
|
|
+ void normalFlow_typesUpdateOperation() {
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.UPDATE),
|
|
|
+ "RED[admin.familyRelation.manage] 控制器缺少关系类型编辑操作"
|
|
|
+ + "(PUT|POST " + API_PREFIX + "/types 或 /types/update):"
|
|
|
+ + "TC-A-10「编辑关系类型 → 操作成功、家长端关系选择器同步生效」。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-5:应提供关系类型停用/删除接口(types · 停用,TC-A-10 + SC-A-04 删)")
|
|
|
+ void normalFlow_typesDisableOperation() {
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.DISABLE),
|
|
|
+ "RED[admin.familyRelation.manage] 控制器缺少关系类型停用/删除操作"
|
|
|
+ + "(DELETE|POST " + API_PREFIX + "/types 或 /types/disable|delete):"
|
|
|
+ + "TC-A-10「停用关系类型」+ SC-A-04「关系类型增删改查 → 生效」。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流:TC-A-10-E(异常) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-1:新增重名关系类型应被拒绝(TC-A-10-E · 名称唯一性契约)")
|
|
|
+ void boundary_duplicateNameRejected() {
|
|
|
+ // 契约:关系类型字典名称唯一,「新增重名类型 → 拒绝」(TC-A-10-E 步骤1 预期)。
|
|
|
+ // 载体(types Create 操作)未落地 → 该行为必然缺位,契约红;
|
|
|
+ // GREEN 转绿条件 = Create 操作存在(名称唯一校验为 GREEN 阶段行为实现点)。
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.CREATE),
|
|
|
+ "RED[admin.familyRelation.manage] 新增接口未落地,'新增重名关系类型应拒绝'行为缺位"
|
|
|
+ + "(TC-A-10-E:重名拒绝;GREEN 阶段 Create 操作须承载名称唯一校验)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-2:停用被引用关系类型需确认/提示先解绑(TC-A-10-E + SC-A-04 边界)")
|
|
|
+ void boundary_referencedTypeDisableNeedsConfirm() {
|
|
|
+ // 契约:关系类型被 family_relationships 引用时,停用/删除需确认或提示先解绑
|
|
|
+ // (SC-A-04 边界「被引用时删需确认」;TC-A-10-E 步骤2「停用被引用类型提示先解绑」)。
|
|
|
+ // 载体(types Disable 操作)未落地 → 该守卫行为必然缺位,契约红;
|
|
|
+ // GREEN 转绿条件 = Disable 操作存在(引用检查为 GREEN 阶段行为实现点)。
|
|
|
+ assertTrue(servesTypeOperation(TypeOp.DISABLE),
|
|
|
+ "RED[admin.familyRelation.manage] 停用/删除接口未落地,'被引用类型停用需先解绑/确认'"
|
|
|
+ + "守卫行为缺位(SC-A-04 边界 + TC-A-10-E 步骤2;"
|
|
|
+ + "GREEN 阶段 Disable 操作须做 family_relationships 引用检查)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 权限流:TC-A-10-P(管理端 401/403 门禁) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流:接口应挂 /api/admin 前缀(JWT 拦截 + 管理员角色门禁,非 admin 统一 401/403)")
|
|
|
+ void permissionFlow_adminApiPrefixGate() {
|
|
|
+ // TC-A-10-P:非 admin 角色访问管理端接口统一 401/403,不泄露后台数据。
|
|
|
+ // 机理由 JwtInterceptor 拦截 /api/admin/** + 控制器内 role=admin 校验承载;
|
|
|
+ // 宿主未落地 → 门禁随之缺位,契约红。
|
|
|
+ List<Class<?>> ctls = discoverAdminFamilyRelationControllers();
|
|
|
+ assertFalse(ctls.isEmpty(),
|
|
|
+ "RED[admin.familyRelation.manage] " + API_PREFIX + " 管理控制器未实现:"
|
|
|
+ + "TC-A-10-P 要求非 admin 访问管理端接口统一 401/403,"
|
|
|
+ + "JwtInterceptor 拦截 /api/admin/** + role=admin 门禁随之缺位。");
|
|
|
+ for (Class<?> c : ctls) {
|
|
|
+ boolean underAdmin = false;
|
|
|
+ for (String p : mappingValues(c.getAnnotations())) {
|
|
|
+ if (p.startsWith("/api/admin")) {
|
|
|
+ underAdmin = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(underAdmin,
|
|
|
+ "RED[admin.familyRelation.manage] 控制器 " + c.getName()
|
|
|
+ + " 类级映射应挂 /api/admin 前缀(JWT 拦截 + 管理员角色门禁,TC-A-10-P)。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 反射辅助(类路径扫描 + 端点语义探测,不假定包路径) ====================
|
|
|
+
|
|
|
+ private enum TypeOp { LIST, CREATE, UPDATE, DISABLE }
|
|
|
+
|
|
|
+ /** 新增语义关键字 */
|
|
|
+ private static final String[] CREATE_KW = {"save", "add", "create", "insert"};
|
|
|
+ /** 编辑语义关键字 */
|
|
|
+ private static final String[] UPDATE_KW = {"update", "edit", "modify", "rename"};
|
|
|
+ /** 停用/删除语义关键字(SC-A-04 删 + TC-A-10 停用) */
|
|
|
+ private static final String[] DISABLE_KW = {"disable", "delete", "remove", "offline", "deactivate"};
|
|
|
+ /** 列表/查询语义关键字 */
|
|
|
+ private static final String[] LIST_KW = {"list", "query", "page", "select", "find", "all"};
|
|
|
+
|
|
|
+ /** 扫描 com.etotem.cfc.controller(含子包递归),筛出类级映射 /api/admin/family-relation 的控制器。 */
|
|
|
+ private List<Class<?>> discoverAdminFamilyRelationControllers() {
|
|
|
+ List<Class<?>> found = new ArrayList<Class<?>>();
|
|
|
+ for (String fqcn : scanControllerClasses()) {
|
|
|
+ Class<?> c = loadClass(fqcn);
|
|
|
+ if (c == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (String p : mappingValues(c.getAnnotations())) {
|
|
|
+ if (p.equals(API_PREFIX) || p.startsWith(API_PREFIX + "/")) {
|
|
|
+ found.add(c);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return found;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 控制器包递归扫描(file: 目录递归 + jar: 直接子类,ClassLoader 资源形态) */
|
|
|
+ private List<String> scanControllerClasses() {
|
|
|
+ List<String> names = new ArrayList<String>();
|
|
|
+ collectPackage(CONTROLLER_BASE_PKG, names);
|
|
|
+ Set<String> unique = new HashSet<String>(names);
|
|
|
+ return new ArrayList<String>(unique);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void collectPackage(String pkg, List<String> out) {
|
|
|
+ try {
|
|
|
+ String path = pkg.replace('.', '/');
|
|
|
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
|
|
+ Enumeration<URL> resources = cl.getResources(path);
|
|
|
+ while (resources.hasMoreElements()) {
|
|
|
+ URL url = resources.nextElement();
|
|
|
+ String protocol = url.getProtocol();
|
|
|
+ if ("file".equals(protocol)) {
|
|
|
+ File dir = new File(url.toURI());
|
|
|
+ File[] files = dir.listFiles();
|
|
|
+ if (files == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (File f : files) {
|
|
|
+ if (f.isDirectory()) {
|
|
|
+ collectPackage(pkg + "." + f.getName(), out);
|
|
|
+ } else if (f.isFile() && f.getName().endsWith(".class")
|
|
|
+ && !f.getName().contains("$")) {
|
|
|
+ out.add(pkg + "." + f.getName().substring(0, f.getName().length() - 6));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if ("jar".equals(protocol)) {
|
|
|
+ // 应用类在 target/classes(file:)下,jar 分支仅兜底取当前包直接子类
|
|
|
+ JarURLConnection conn = (JarURLConnection) url.openConnection();
|
|
|
+ try (JarFile jar = conn.getJarFile()) {
|
|
|
+ String prefix = path + "/";
|
|
|
+ Enumeration<java.util.jar.JarEntry> entries = jar.entries();
|
|
|
+ while (entries.hasMoreElements()) {
|
|
|
+ java.util.jar.JarEntry e = entries.nextElement();
|
|
|
+ String n = e.getName();
|
|
|
+ if (n.startsWith(prefix) && n.endsWith(".class")
|
|
|
+ && !n.substring(prefix.length()).contains("$")
|
|
|
+ && n.indexOf('/', prefix.length()) < 0) {
|
|
|
+ out.add(n.substring(0, n.length() - 6).replace('/', '.'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 包扫描失败 → 按缺失处理(外层断言会红,扫描器自检用例可定位)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 该方法是否为关系类型(types)管理端点:类级或方法级映射含 types 段。 */
|
|
|
+ private boolean targetsTypes(Class<?> ctrl, Method m) {
|
|
|
+ if (mappingValues(m.getAnnotations()).isEmpty()) {
|
|
|
+ return false; // 无映射注解的方法不是端点
|
|
|
+ }
|
|
|
+ for (String p : mappingValues(ctrl.getAnnotations())) {
|
|
|
+ if (p.contains(TYPES)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String p : mappingValues(m.getAnnotations())) {
|
|
|
+ if (p.contains(TYPES)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 关系类型操作语义分类:类型管理端点 → LIST/CREATE/UPDATE/DISABLE;非类型端点 → null。 */
|
|
|
+ private TypeOp classifyTypeOp(Class<?> ctrl, Method m) {
|
|
|
+ if (!targetsTypes(ctrl, m)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ StringBuilder sb = new StringBuilder(m.getName());
|
|
|
+ sb.append(' ');
|
|
|
+ for (Annotation a : m.getAnnotations()) {
|
|
|
+ sb.append(a.annotationType().getSimpleName()).append(' ');
|
|
|
+ }
|
|
|
+ for (String p : mappingValues(m.getAnnotations())) {
|
|
|
+ sb.append(p).append(' ');
|
|
|
+ }
|
|
|
+ for (String p : mappingValues(ctrl.getAnnotations())) {
|
|
|
+ sb.append(p).append(' ');
|
|
|
+ }
|
|
|
+ String sig = sb.toString().toLowerCase();
|
|
|
+
|
|
|
+ if (containsAny(sig, CREATE_KW)) {
|
|
|
+ return TypeOp.CREATE;
|
|
|
+ }
|
|
|
+ if (containsAny(sig, UPDATE_KW)) {
|
|
|
+ return TypeOp.UPDATE;
|
|
|
+ }
|
|
|
+ if (containsAny(sig, DISABLE_KW)) {
|
|
|
+ return TypeOp.DISABLE;
|
|
|
+ }
|
|
|
+ if (containsAny(sig, LIST_KW) || sig.trim().startsWith("get")) {
|
|
|
+ return TypeOp.LIST;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 已发现的控制器中是否存在某类关系类型操作。 */
|
|
|
+ private boolean servesTypeOperation(TypeOp op) {
|
|
|
+ for (Class<?> c : discoverAdminFamilyRelationControllers()) {
|
|
|
+ for (Method m : c.getDeclaredMethods()) {
|
|
|
+ if (classifyTypeOp(c, m) == op) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean containsAny(String s, String[] keywords) {
|
|
|
+ for (String kw : keywords) {
|
|
|
+ if (s.contains(kw)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 读取 Spring 映射注解(@RequestMapping/@GetMapping/@PostMapping/@PutMapping/@DeleteMapping)的 value() 路径集合。 */
|
|
|
+ private List<String> mappingValues(Annotation[] annotations) {
|
|
|
+ List<String> paths = new ArrayList<String>();
|
|
|
+ for (Annotation a : annotations) {
|
|
|
+ String tn = a.annotationType().getName();
|
|
|
+ if (tn.equals("org.springframework.web.bind.annotation.RequestMapping")
|
|
|
+ || tn.equals("org.springframework.web.bind.annotation.PostMapping")
|
|
|
+ || tn.equals("org.springframework.web.bind.annotation.GetMapping")
|
|
|
+ || tn.equals("org.springframework.web.bind.annotation.PutMapping")
|
|
|
+ || tn.equals("org.springframework.web.bind.annotation.DeleteMapping")) {
|
|
|
+ try {
|
|
|
+ Object v = a.annotationType().getMethod("value").invoke(a);
|
|
|
+ if (v instanceof String[]) {
|
|
|
+ for (String s : (String[]) v) {
|
|
|
+ paths.add(s);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 非路径型映射注解,忽略
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return paths;
|
|
|
+ }
|
|
|
+}
|