Files
notes/work/移动杭研/业务梳理/信安信息/信安信息递归问题.md
T
2026-03-01 01:43:46 +08:00

6.2 KiB
Raw Blame History



  ---
  【核心判断】

  🔴 这是垃圾代码 - 复杂、低效、有隐藏的递归炸弹

  ---
  【流程分析:a.b.c.tttt.cn 每层都没备案会怎样】

  假设主域名集合包含 cn,那么:

  第1轮:直接查询

  查询: a.b.c.tttt.cn
  返回: false (未备案)
  ↓
  计算主域名: tttt.cn
  生成子域名列表: [b.c.tttt.cn, c.tttt.cn, tttt.cn]
  进入第2轮 ↓

  第2轮:递归查子域名(第258行的递归陷阱)

  查询: [b.c.tttt.cn, c.tttt.cn, tttt.cn]
  全部返回: false
  ↓ 分别处理:

  b.c.tttt.cn → 生成 [c.tttt.cn, tttt.cn]
  c.tttt.cn   → 生成 [tttt.cn]
  tttt.cn     → 生成 []  (空!递归终止条件)

  合并后查询列表: [c.tttt.cn, tttt.cn, tttt.cn]  ← 注意重复!
  进入第3轮 ↓

  第3轮:继续递归

  查询: [c.tttt.cn, tttt.cn, tttt.cn]  ← tttt.cn被查了2次!
  全部返回: false
  ↓
  c.tttt.cn → 生成 [tttt.cn]
  tttt.cn   → 生成 []
  tttt.cn   → 生成 []

  合并后查询列表: [tttt.cn]
  进入第4轮 ↓

  第4轮:最后一次

  查询: [tttt.cn]
  返回: false
  ↓
  tttt.cn → 生成 []

  递归结束!

  最终结果

  icpMap = {
      "a.b.c.tttt.cn": "false"  // 186行的逻辑会用最后一次子域名查询结果覆盖
  }

  ---
  【致命问题】

  1️⃣ 隐藏的递归炸弹 (XinAnApiService.java:258)

  Map<String, String> domainsICP = getDomainsIcp(subDomains);  // 递归!

  这他妈是在干什么?
  - 方法 getDomainsIcp 调用 getSubDomainIcp
  - getSubDomainIcp 又调用回 getDomainsIcp
  - 形成递归,但没有任何注释说明!
  - 递归深度 = 域名层级数 - 1

  对于 a.b.c.d.e.f.g.tttt.cn8层域名):
  - 会递归 7 次
  - 总查询次数:1 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 28 次调用信安接口!
  - 而且 tttt.cn 会被重复查询 7 次!

  2️⃣ 数据结构错了 - 没有去重!

  List<String> subDomains = Lists.newArrayList();
  subDomainMap.values().forEach(subDomains::addAll);  // 256-257行:直接addAll,没去重!

  tttt.cn 会在 subDomains 列表中出现多次,每次都要查询信安接口。

  "Bad programmers worry about the code. Good programmers worry about data structures."
  - 你需要的是 Set<String>,而不是 List<String>
  - 或者在查询前去重:subDomains.stream().distinct().collect(Collectors.toList())

  3️⃣ 复杂度爆炸 - 3层缩进是底线,这代码有多少层?

  看看第147-158行的嵌套:
  for (Map.Entry<String, String> entry : icpMap.entrySet()) {  // 第1层
      if (StringUtils.isEmpty(icp) || FALSE_ICP.getValue().equals(icp)) {  // 第2层
          if (CollectionUtils.isEmpty(subDomains)) {  // 第3层
              continue;
          }
      }
  }

  还有异常处理里又重复了一遍逻辑(159-170行)—— 这是什么鬼设计?

  4️⃣ getSubDomain 的诡异逻辑 (XinAnApiService.java:358-367)

  temp.forEach(var ->
      subDomains.addFirst(StringUtils.join(Arrays.asList(var, subDomains.getFirst()), SEPARATOR2))
  );
  subDomains.remove(subDomains.getFirst());  // 为什么要先加再删?

  为什么不一开始就不加原域名?
  - 这是在解决不存在的问题
  - 应该直接构建正确的列表,而不是"先加后删"

  ---
  【Linus式重构方案】

  核心思想:消除递归,用迭代 + 去重

  public Map<String, String> getDomainsIcp(List<String> domains) {
      if (CollectionUtils.isEmpty(domains)) {
          return Maps.newHashMap();
      }

      // 第1步:收集所有需要查询的域名(包括子域名),用 Set 去重
      Set<String> allDomainsToQuery = new LinkedHashSet<>();
      Map<String, List<String>> domainHierarchy = new HashMap<>();

      for (String domain : domains) {
          allDomainsToQuery.add(domain);
          String mainDomain = getDomainParam(domain);
          List<String> subDomains = getSubDomain(mainDomain, domain);
          allDomainsToQuery.addAll(subDomains);  // Set自动去重
          domainHierarchy.put(domain, subDomains);
      }

      // 第2步:一次性查询所有域名(去重后)
      Map<String, String> allIcpResults = queryDomainsIcpByGroup(new ArrayList<>(allDomainsToQuery));

      // 第3步:为每个原始域名找最近的有备案的父域名
      Map<String, String> result = new HashMap<>();
      for (String domain : domains) {
          String icp = allIcpResults.get(domain);

          // 如果自己有备案,直接用
          if (icp != null && !icp.equals(FALSE_ICP.getValue()) && !icp.isEmpty()) {
              result.put(domain, icp);
              continue;
          }

          // 否则从子域名列表中找第一个有备案的
          List<String> subDomains = domainHierarchy.get(domain);
          boolean found = false;
          if (subDomains != null) {
              for (String subDomain : subDomains) {
                  String subIcp = allIcpResults.get(subDomain);
                  if (subIcp != null && !subIcp.equals(FALSE_ICP.getValue()) && !subIcp.isEmpty()) {
                      result.put(domain, subIcp);
                      found = true;
                      break;
                  }
              }
          }

          if (!found) {
              result.put(domain, icp != null ? icp : "");
          }
      }

      return result;
  }

  改进效果:

  | 指标           | 原代码           | 重构后       |
  |--------------|---------------|-----------|
  | 递归次数         | N层            | 0         |
  | 接口调用次数(8层域名) | 28次           | 8次(去重后)   |
  | 重复查询         | 是             | 否         |
  | 代码行数         | ~80行(分散在3个方法) | ~40行(单方法) |
  | 可读性          | 需要追踪递归        | 一目了然      |

  ---
  【最后警告】

  这段代码有三个经典反模式:

  1. "聪明"代码 - 递归隐藏在方法调用里,不看完整代码根本发现不了
  2. 重复逻辑 - 异常处理块里复制粘贴了主逻辑
  3. 过度抽象 - getSubDomain 方法可以简化成3行,但写成了10行

  "Talk is cheap. Show me the code." - 现在代码给你了,你看着办吧。

  需要我帮你重构吗?还是你有其他问题?