| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- tools/check_circular_deps.py — CFC 后端 Spring 循环依赖静态检查
- 用途
- ----
- 在提交前静态解析 cfc-backend/src/main/java 的全部 Java 源码,构建「Bean 依赖图」,
- 区分 **eager 边**(启动期立即实例化)与 **@Lazy 边**(延迟代理,启动期不实例化),
- 输出:
- 1. eager 循环依赖清单 —— 启动期致命,必须为空;非空时退出码 1(阻断提交)
- 2. @Lazy 掩盖的设计环台账 —— 允许存在(既有 30+ 条),仅登记不阻断
- 3. @Lazy 注入点清单 —— 供人工复核「@Lazy 仅在字段名与 Bean 名严格一致时使用」
- 解析能力
- --------
- * 字段注入:@Resource / @Autowired(含 @Lazy、@Qualifier、@Resource(name="..."))
- * setter 注入:方法上的 @Autowired / @Resource
- * 构造器注入:构造器上的 @Autowired / @Resource
- * @Resource 按 by-name 解析(by-name 失败回退 by-type)
- * @Autowired 按 by-type 解析;接口类型回退到「唯一实现类」
- * Bean 识别:@Service / @Component / @Repository / @Controller / @RestController /
- @Configuration / com.etotem.cfc.mapper 下的 Mapper 接口
- 退出码
- ------
- 0 —— 未发现 eager 循环依赖
- 1 —— 发现 eager 循环依赖(必须从设计上打破,禁止用 @Lazy 掩盖新环)
- 2 —— 脚本自身错误(源码目录不存在等)
- 用法
- ----
- python3 tools/check_circular_deps.py
- """
- import os
- import re
- import sys
- from collections import defaultdict
- # --------------------------------------------------------------------------- #
- # 路径与常量
- # --------------------------------------------------------------------------- #
- ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
- SRC_DIR = os.path.join(ROOT, "cfc-backend", "src", "main", "java")
- STEREOTYPE_SIMPLE = {"Service", "Component", "Repository", "Controller",
- "RestController", "Configuration"}
- MAPPER_PACKAGE_PREFIX = "com.etotem.cfc.mapper"
- INJECT_ANNOTATIONS = ("@Resource", "@Autowired")
- LAZY_ANNOTATION = "@Lazy"
- # 集合/容器注入:Spring 注入的是「多个候选」,无法归到单一 Bean 节点,解析时跳过
- COLLECTION_TYPES = {
- "List", "Set", "Map", "Collection", "Iterable", "ArrayList", "LinkedList",
- "HashSet", "TreeSet", "HashMap", "LinkedHashMap", "TreeMap", "Optional",
- "ObjectProvider", "Multimap",
- }
- # --------------------------------------------------------------------------- #
- # 源码预处理
- # --------------------------------------------------------------------------- #
- def strip_comments(src):
- """把注释替换为等长空格(保留换行与偏移),避免注释中的注解造成误判。"""
- out = list(src)
- i, n = 0, len(src)
- state = None # None / 'line' / 'block' / 'str' / 'char'
- while i < n:
- c = src[i]
- nxt = src[i + 1] if i + 1 < n else ""
- if state is None:
- if c == "/" and nxt == "/":
- state = "line"
- out[i] = out[i + 1] = " "
- i += 2
- continue
- if c == "/" and nxt == "*":
- state = "block"
- out[i] = out[i + 1] = " "
- i += 2
- continue
- if c == '"':
- state = "str"
- i += 1
- continue
- if c == "'":
- state = "char"
- i += 1
- continue
- i += 1
- continue
- if state == "line":
- if c == "\n":
- state = None
- else:
- out[i] = " "
- i += 1
- continue
- if state == "block":
- if c == "*" and nxt == "/":
- out[i] = out[i + 1] = " "
- state = None
- i += 2
- continue
- if c != "\n":
- out[i] = " "
- i += 1
- continue
- if state in ("str", "char"):
- quote = '"' if state == "str" else "'"
- if c == "\\":
- i += 2
- continue
- if c == quote:
- state = None
- i += 1
- continue
- return "".join(out)
- def decapitalize(name):
- """Spring 默认 Bean 名:首字母小写(前两字母均大写则保持原样)。"""
- if not name:
- return name
- if len(name) > 1 and name[0].isupper() and name[1].isupper():
- return name
- return name[0].lower() + name[1:]
- def strip_generics(type_text):
- """List<Foo> -> List;Map<String, Foo> -> Map"""
- depth = 0
- out = []
- for ch in type_text:
- if ch == "<":
- depth += 1
- continue
- if ch == ">":
- depth = max(0, depth - 1)
- continue
- if depth == 0:
- out.append(ch)
- return "".join(out).strip()
- def simple_type(type_text):
- """取类型简单名(去包名、泛型、数组)"""
- t = strip_generics(type_text).strip()
- t = t.split(".")[-1].strip()
- while t.endswith("[]"):
- t = t[:-2].strip()
- t = t.replace("...", "").strip()
- return t
- # --------------------------------------------------------------------------- #
- # 类型声明解析
- # --------------------------------------------------------------------------- #
- TYPE_DECL_RE = re.compile(
- r"(?P<prefix>[^{};]*?)\b(?P<kind>class|interface|enum)\s+(?P<name>\w+)"
- r"(?P<rest>[^{};]*?)\{"
- )
- class TypeDecl(object):
- __slots__ = ("name", "kind", "package", "file", "prefix", "rest", "body_start", "body_end")
- def __init__(self, name, kind, package, file, prefix, rest, body_start, body_end):
- self.name = name
- self.kind = kind
- self.package = package
- self.file = file
- self.prefix = prefix
- self.rest = rest
- self.body_start = body_start
- self.body_end = body_end
- def parse_package(src):
- m = re.search(r"^\s*package\s+([\w.]+)\s*;", src, re.M)
- return m.group(1) if m else ""
- def parse_type_decls(src, file_path):
- decls = []
- package = parse_package(src)
- for m in TYPE_DECL_RE.finditer(src):
- prefix = m.group("prefix")
- # 仅认列 0 开始的顶层声明(缩进声明视为方法体内的局部类/匿名类,跳过)
- last_line = prefix.split("\n")[-1] if prefix else ""
- if last_line[:1] in (" ", "\t"):
- continue
- body_start = m.end()
- depth = 1
- i = body_start
- while i < len(src) and depth > 0:
- if src[i] == "{":
- depth += 1
- elif src[i] == "}":
- depth -= 1
- i += 1
- decls.append(TypeDecl(
- name=m.group("name"),
- kind=m.group("kind"),
- package=package,
- file=file_path,
- prefix=prefix,
- rest=m.group("rest"),
- body_start=body_start,
- body_end=i - 1,
- ))
- return decls
- def prefix_annotation_names(prefix):
- """提取声明前注解块中的注解简单名(兼容全限定写法)"""
- return [m.split(".")[-1] for m in re.findall(r"@\s*([\w.]+)", prefix)]
- def is_stereotype(prefix):
- """类声明前的注解块里是否含 Spring stereotype 注解"""
- return bool(STEREOTYPE_SIMPLE & set(prefix_annotation_names(prefix)))
- def stereotype_name(prefix):
- """解析 @Service("xxx") / @Component("xxx") 显式 Bean 名"""
- for m in re.finditer(r"@\s*([\w.]+)(?:\s*\(\s*\"([^\"]+)\"\s*\))", prefix):
- if m.group(1).split(".")[-1] in STEREOTYPE_SIMPLE:
- return m.group(2)
- return None
- # --------------------------------------------------------------------------- #
- # 注入点解析
- # --------------------------------------------------------------------------- #
- class InjectionPoint(object):
- __slots__ = ("owner", "kind", "field", "type", "annotation", "lazy", "file", "line", "explicit_name")
- def __init__(self, owner, kind, field, type_, annotation, lazy, file, line, explicit_name=None):
- self.owner = owner
- self.kind = kind # field / setter / constructor
- self.field = field
- self.type = type_
- self.annotation = annotation # @Resource / @Autowired
- self.lazy = lazy
- self.file = file
- self.line = line
- self.explicit_name = explicit_name
- def describe(self):
- loc = "%s:%d" % (os.path.relpath(self.file, ROOT), self.line)
- return "%s.%s (%s %s%s) -> %s [%s]" % (
- self.owner, self.field, self.annotation,
- "@Lazy " if self.lazy else "",
- self.type, loc, self.kind)
- ANNOTATION_RE = re.compile(
- r"\s*@\s*(?P<name>[\w.]+)(?:\s*\((?P<args>(?:[^()]|\([^()]*\))*)\))?")
- def skip_annotations(src, pos):
- """跳过紧邻的注解块,返回 (新位置, 注解列表)"""
- anns = []
- while True:
- m = ANNOTATION_RE.match(src, pos)
- if not m:
- break
- anns.append((m.group("name"), m.group("args") or ""))
- pos = m.end()
- return pos, anns
- def find_next_token(src, pos, tokens=(";", "{", "=")):
- """找到下一个顶层分隔符(跳过字符串/字符字面量与括号)"""
- depth = 0
- i = pos
- n = len(src)
- while i < n:
- c = src[i]
- if c in "\"'":
- quote = c
- i += 1
- while i < n:
- if src[i] == "\\":
- i += 2
- continue
- if src[i] == quote:
- break
- i += 1
- i += 1
- continue
- if c in "([{":
- depth += 1
- elif c in ")]}":
- depth -= 1
- elif depth == 0 and c in tokens:
- return c, i
- i += 1
- return None, n
- def parse_field_decl(text):
- """'private Foo bar' -> ('Foo', 'bar');'private List<Foo> bars' -> ('List', 'bars')"""
- text = text.strip()
- if not text:
- return None, None
- text = re.sub(r"^(?:public|protected|private|static|final|transient|volatile)\s+", "", text)
- text = text.strip()
- if not text:
- return None, None
- # 泛型整体作为一个 token
- m = re.match(r"^(?P<type>[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])*)\s+(?P<name>\w+)\s*$", text, re.S)
- if not m:
- return None, None
- return m.group("type"), m.group("name")
- def split_params(param_text):
- """按顶层逗号切分参数列表"""
- parts, buf, depth = [], [], 0
- for ch in param_text:
- if ch in "<([":
- depth += 1
- elif ch in ">)]":
- depth -= 1
- if ch == "," and depth == 0:
- parts.append("".join(buf))
- buf = []
- else:
- buf.append(ch)
- if "".join(buf).strip():
- parts.append("".join(buf))
- return parts
- def parse_method_params(text):
- """解析方法/构造器参数 -> [(type, name), ...]"""
- idx = text.find("(")
- if idx < 0:
- return []
- start = idx + 1
- depth = 1
- i = start
- while i < len(text) and depth > 0:
- if text[i] in "([{":
- depth += 1
- elif text[i] in ")]}":
- depth -= 1
- i += 1
- inner = text[start:i - 1]
- result = []
- for raw in split_params(inner):
- p = raw.strip()
- if not p:
- continue
- p = re.sub(r"^(?:final|@\w+(\([^)]*\))?)\s+", "", p)
- p = re.sub(r"^(?:public|protected|private)\s+", "", p).strip()
- tokens = re.findall(r"[\w.]+(?:\s*<.*?>)?(?:\s*\[\s*\])?|\w+", p)
- if not tokens:
- continue
- if len(tokens) == 1:
- result.append((tokens[0], tokens[0]))
- else:
- result.append((" ".join(tokens[:-1]), tokens[-1]))
- return result
- def scan_class_body(src, decl):
- """扫描类体(顶层成员)中的注入点"""
- points = []
- body = src[decl.body_start:decl.body_end]
- offset = decl.body_start
- i = 0
- depth = 0
- n = len(body)
- while i < n:
- c = body[i]
- if c in "\"'":
- quote = c
- i += 1
- while i < n:
- if body[i] == "\\":
- i += 2
- continue
- if body[i] == quote:
- break
- i += 1
- i += 1
- continue
- if c == "{":
- depth += 1
- i += 1
- continue
- if c == "}":
- depth -= 1
- i += 1
- continue
- if depth == 0 and c == "@":
- # 收集连续的注解块(顺序不限:@Lazy @Autowired / @Autowired @Lazy / @Resource @Lazy)
- block_start = i
- pos = i
- block = []
- while True:
- m = ANNOTATION_RE.match(body, pos)
- if not m:
- break
- block.append((m.group("name"), m.group("args") or ""))
- pos = m.end()
- if not block:
- i += 1
- continue
- # 注解名归一为简单名(兼容 @org.springframework.context.annotation.Lazy 等全限定写法)
- simple_names = [a[0].split(".")[-1] for a in block]
- inject_ann = None
- if "Resource" in simple_names:
- inject_ann = "Resource"
- elif "Autowired" in simple_names:
- inject_ann = "Autowired"
- if inject_ann is None:
- i = max(pos, block_start + 1)
- continue
- lazy = "Lazy" in simple_names
- explicit_name = None
- for aname, aargs in block:
- if aname.split(".")[-1] in ("Resource", "Autowired"):
- mn = re.search(r"name\s*=\s*\"([^\"]+)\"", aargs or "")
- if mn:
- explicit_name = mn.group(1)
- after = pos
- # 找到声明
- tok, tok_pos = find_next_token(body, after)
- if tok is None:
- i = after
- continue
- seg = body[after:tok_pos]
- if tok in (";", "="):
- type_text, field = parse_field_decl(seg)
- if type_text and field:
- points.append(InjectionPoint(
- owner=decl.name, kind="field", field=field,
- type_=simple_type(type_text), annotation="@" + inject_ann,
- lazy=lazy, file=decl.file,
- line=src.count("\n", 0, offset + after) + 1,
- explicit_name=explicit_name))
- elif tok == "{":
- params = parse_method_params(seg)
- is_ctor = re.search(r"\b%s\s*\(" % re.escape(decl.name), seg) is not None
- for ptype, pname in params:
- if ptype.startswith("this"):
- continue
- points.append(InjectionPoint(
- owner=decl.name, kind="constructor" if is_ctor else "setter",
- field=pname, type_=simple_type(ptype), annotation="@" + inject_ann,
- lazy=lazy, file=decl.file,
- line=src.count("\n", 0, offset + after) + 1,
- explicit_name=explicit_name))
- i = max(after, block_start + 1)
- continue
- i += 1
- return points
- # --------------------------------------------------------------------------- #
- # Bean 注册表与依赖解析
- # --------------------------------------------------------------------------- #
- class Registry(object):
- def __init__(self):
- self.bean_by_name = {} # bean name -> TypeDecl
- self.classes_by_simple = defaultdict(list) # simple name -> [TypeDecl]
- self.interfaces = {} # simple name -> TypeDecl(kind=interface)
- self.implementations = defaultdict(list) # interface simple name -> [TypeDecl]
- def add(self, decl, bean_name):
- self.bean_by_name[bean_name] = decl
- self.classes_by_simple[decl.name].append(decl)
- if decl.kind == "interface":
- self.interfaces[decl.name] = decl
- def link(self):
- for decl in list(self.classes_by_simple.values()):
- pass
- for names in self.classes_by_simple.values():
- for decl in names:
- rest = decl.rest or ""
- m = re.search(r"\bimplements\s+([^{]+)", rest)
- if m:
- for iface in split_params(m.group(1)):
- iface = re.sub(r"<.*?>", "", iface).strip().split(".")[-1]
- if iface:
- self.implementations[iface].append(decl)
- m2 = re.search(r"\bextends\s+([^{]+)", rest)
- if m2 and decl.kind == "interface":
- for parent in split_params(m2.group(1)):
- parent = re.sub(r"<.*?>", "", parent).strip().split(".")[-1]
- if parent:
- self.implementations[parent].append(decl)
- def resolve(self, point):
- """返回 (目标 TypeDecl 列表, 解析方式);多实现时保守全连边(宁可多报不漏报)"""
- if point.type in COLLECTION_TYPES:
- return [], "collection"
- # 1) 显式 name:@Resource(name="x") / @Autowired(name="x")
- if point.explicit_name and point.explicit_name in self.bean_by_name:
- return [self.bean_by_name[point.explicit_name]], "by-name(explicit)"
- # 2) @Resource by-name:字段名 = Bean 名(本项目强约束)
- if point.annotation == "@Resource" and point.field in self.bean_by_name:
- return [self.bean_by_name[point.field]], "by-name"
- # 3) by-type:类型简单名直接命中 Bean 类
- cands = self.classes_by_simple.get(point.type, [])
- if len(cands) == 1:
- return cands, "by-type"
- if len(cands) > 1:
- return [], "ambiguous-type"
- # 4) 接口 → 实现类
- impls = self.implementations.get(point.type, [])
- if len(impls) == 1:
- return impls, "by-type(iface->impl)"
- if len(impls) > 1:
- primary = [d for d in impls if "@Primary" in (d.prefix or "")]
- if len(primary) == 1:
- return primary, "by-primary"
- return impls, "ambiguous-iface"
- return [], "unresolved"
- # --------------------------------------------------------------------------- #
- # 图算法
- # --------------------------------------------------------------------------- #
- def tarjan_scc(nodes, adj):
- """返回 SCC 列表(迭代版 Tarjan)"""
- index = {}
- low = {}
- on_stack = {}
- stack = []
- result = []
- counter = [0]
- for root in nodes:
- if root in index:
- continue
- work = [(root, iter(adj.get(root, ())))]
- index[root] = low[root] = counter[0]
- counter[0] += 1
- stack.append(root)
- on_stack[root] = True
- while work:
- v, it = work[-1]
- advanced = False
- for w in it:
- if w not in index:
- index[w] = low[w] = counter[0]
- counter[0] += 1
- stack.append(w)
- on_stack[w] = True
- work.append((w, iter(adj.get(w, ()))))
- advanced = True
- break
- if on_stack.get(w):
- low[v] = min(low[v], index[w])
- if advanced:
- continue
- work.pop()
- if work:
- u = work[-1][0]
- low[u] = min(low[u], low[v])
- if low[v] == index[v]:
- comp = []
- while True:
- w = stack.pop()
- on_stack[w] = False
- comp.append(w)
- if w == v:
- break
- result.append(comp)
- return result
- def find_cycle_within(nodes, adj):
- """在给定节点集合内寻找一条简单环(DFS),返回路径列表"""
- node_set = set(nodes)
- for start in sorted(nodes):
- stack = [(start, [start])]
- visited = set()
- while stack:
- node, path = stack.pop()
- for nxt in adj.get(node, ()):
- if nxt == start:
- return path + [start]
- if nxt in node_set and nxt not in visited:
- visited.add(nxt)
- stack.append((nxt, path + [nxt]))
- return None
- def enumerate_cycles(nodes, adj, limit=100):
- """枚举节点集合内的全部简单环(从环上最小节点起步,保证每环只枚举一次)"""
- node_set = set(nodes)
- cycles = []
- for start in sorted(nodes):
- if len(cycles) >= limit:
- break
- stack = [(start, [start])]
- while stack:
- if len(cycles) >= limit:
- break
- node, path = stack.pop()
- for nxt in sorted(adj.get(node, ())):
- if nxt == start:
- cycles.append(path + [start])
- if len(cycles) >= limit:
- break
- elif nxt in node_set and nxt not in path and nxt >= start:
- stack.append((nxt, path + [nxt]))
- return cycles
- def find_path(src, dst, adj):
- """BFS 最短路径 src → dst(含端点),不可达返回 None"""
- if src == dst:
- return [src]
- prev = {src: None}
- queue = [src]
- head = 0
- while head < len(queue):
- node = queue[head]
- head += 1
- for nxt in sorted(adj.get(node, ())):
- if nxt in prev:
- continue
- prev[nxt] = node
- if nxt == dst:
- path = [dst]
- cur = node
- while cur is not None:
- path.append(cur)
- cur = prev[cur]
- path.reverse()
- return path
- queue.append(nxt)
- return None
- # --------------------------------------------------------------------------- #
- # 主流程
- # --------------------------------------------------------------------------- #
- def collect_types():
- registry = Registry()
- decls = []
- for dirpath, _dirnames, filenames in os.walk(SRC_DIR):
- for fn in filenames:
- if not fn.endswith(".java"):
- continue
- path = os.path.join(dirpath, fn)
- with open(path, "r", encoding="utf-8", errors="replace") as fh:
- raw = fh.read()
- src = strip_comments(raw)
- file_decls = parse_type_decls(src, path)
- for d in file_decls:
- decls.append((src, d))
- # Mapper 接口(MyBatis-Plus 扫描 com.etotem.cfc.mapper)
- if d.kind == "interface" and d.package.startswith(MAPPER_PACKAGE_PREFIX):
- registry.add(d, decapitalize(d.name))
- elif is_stereotype(d.prefix):
- explicit = stereotype_name(d.prefix)
- registry.add(d, explicit or decapitalize(d.name))
- registry.link()
- return registry, decls
- def build_graph(registry, decls):
- edges = [] # (owner, target, point)
- skipped = 0 # 实体/POJO/无法解析(非 Bean,忽略)
- ambiguous = [] # 接口多实现,按保守全连边(可能多报)
- for src, decl in decls:
- if decl.name not in registry.classes_by_simple:
- continue
- if len(registry.classes_by_simple[decl.name]) != 1:
- continue
- if not (is_stereotype(decl.prefix) or
- (decl.kind == "interface" and decl.package.startswith(MAPPER_PACKAGE_PREFIX))):
- continue
- points = scan_class_body(src, decl)
- for p in points:
- targets, how = registry.resolve(p)
- if not targets:
- if how in ("ambiguous-type",):
- ambiguous.append((p, how))
- else:
- skipped += 1
- continue
- if how == "ambiguous-iface":
- ambiguous.append((p, how))
- for t in targets:
- edges.append((decl, t, p))
- return edges, skipped, ambiguous
- def main():
- if not os.path.isdir(SRC_DIR):
- sys.stderr.write("[check_circular_deps] 源码目录不存在: %s\n" % SRC_DIR)
- return 2
- registry, decls = collect_types()
- edges, skipped, ambiguous = build_graph(registry, decls)
- # 邻接表
- eager_adj = defaultdict(set)
- full_adj = defaultdict(set)
- edge_info = {}
- for owner, target, p in edges:
- if owner.name == target.name:
- continue
- kind = "lazy" if p.lazy else "eager"
- eager_adj[owner.name].add(target.name) if kind == "eager" else None
- full_adj[owner.name].add(target.name)
- edge_info[(owner.name, target.name)] = (kind, p)
- class_nodes = set()
- for decls_of_same_simple_name in registry.classes_by_simple.values():
- for decl in decls_of_same_simple_name:
- class_nodes.add(decl.name)
- # ---- 1. eager 环(全部简单环) ----
- eager_sccs = tarjan_scc(class_nodes, eager_adj)
- eager_cycles = []
- for comp in eager_sccs:
- if len(comp) > 1:
- sub = {n: [t for t in eager_adj.get(n, ()) if t in comp] for n in comp}
- eager_cycles.extend(enumerate_cycles(comp, sub))
- elif comp:
- node = comp[0]
- if node in eager_adj.get(node, ()):
- eager_cycles.append([node, node])
- # ---- 2. @Lazy 掩盖的设计环:逐条 @Lazy 边判断是否位于某个环上 ----
- design_cycles = []
- seen_design = set()
- for (u, v), (kind, _p) in sorted(edge_info.items()):
- if kind != "lazy":
- continue
- path = find_path(v, u, full_adj)
- if not path:
- continue # 该 @Lazy 边不在任何环上
- cycle = [u] + path
- key = frozenset(cycle[:-1])
- if key in seen_design:
- continue
- seen_design.add(key)
- design_cycles.append(cycle)
- # ---- 3. @Lazy 注入点清单 ----
- lazy_points = [p for (_o, _t, p) in edges if p.lazy]
- lazy_points.sort(key=lambda p: (p.owner, p.field))
- # ---- 输出 ----
- out = []
- out.append("=" * 72)
- out.append("CFC 后端循环依赖静态检查 (tools/check_circular_deps.py)")
- out.append("=" * 72)
- out.append("源码目录 : %s" % os.path.relpath(SRC_DIR, ROOT))
- out.append("Java 文件数 : %d" % sum(1 for _d, _f, files in os.walk(SRC_DIR) for f in files if f.endswith(".java")))
- out.append("识别 Bean 数 : %d" % len(registry.bean_by_name))
- out.append("注入点总数 : %d (eager %d / lazy %d)"
- % (len(edges), len(edges) - len(lazy_points), len(lazy_points)))
- out.append("[1] Eager 循环依赖(启动期致命,必须为空)")
- if eager_cycles:
- for i, cyc in enumerate(eager_cycles, 1):
- out.append(" ✗ 环 %d: %s" % (i, " → ".join(cyc)))
- for a, b in zip(cyc, cyc[1:]):
- info = edge_info.get((a, b))
- if info:
- out.append(" %s --%s--> %s" % (a, info[0], b))
- else:
- out.append(" ✅ 未发现 eager 循环依赖")
- out.append("")
- out.append("[2] @Lazy 掩盖的设计环台账(既有设计债,仅登记不阻断)")
- if design_cycles:
- for i, cyc in enumerate(design_cycles, 1):
- lazy_edges = []
- for a, b in zip(cyc, cyc[1:]):
- info = edge_info.get((a, b))
- if info and info[0] == "lazy":
- lazy_edges.append("%s → %s" % (a, b))
- out.append(" • 环 %d: %s" % (i, " → ".join(cyc)))
- if lazy_edges:
- out.append(" @Lazy 断点: %s" % ", ".join(lazy_edges))
- else:
- out.append(" (无)")
- out.append("")
- out.append("[3] @Lazy 注入点清单(字段名须与 Bean 名严格一致)")
- if lazy_points:
- for p in lazy_points:
- out.append(" - %s.%s (%s %s) -> %s [%s:%d]"
- % (p.owner, p.field, p.annotation,
- "@Lazy" if p.lazy else "", p.type,
- os.path.relpath(p.file, ROOT), p.line))
- else:
- out.append(" (无)")
- out.append("")
- if ambiguous:
- out.append("[4] 接口多实现(按保守全连边,可能产生额外环提示):%d 条" % len(ambiguous))
- seen = {}
- for p, how in ambiguous:
- key = (p.owner, p.field, p.type)
- seen.setdefault(key, p)
- for p in list(seen.values())[:15]:
- out.append(" - %s.%s -> %s (ambiguous-iface)" % (p.owner, p.field, p.type))
- else:
- out.append("[4] 所有注入点均唯一解析到 Bean")
- if skipped:
- out.append(" 另有 %d 个注入点未解析(实体/POJO/容器类型,正常忽略)" % skipped)
- out.append("")
- if eager_cycles:
- out.append("结论: ❌ 发现 %d 条 eager 循环依赖 —— 必须从设计上打破"
- "(抽窄接口 / 下沉 / 事件解耦),禁止用 @Lazy 掩盖新环。" % len(eager_cycles))
- out.append("=" * 72)
- print("\n".join(out))
- return 1
- out.append("结论: ✅ eager 环 = 0(通过)")
- out.append("=" * 72)
- print("\n".join(out))
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|