From f5d44100635c093de3f22b4f74f13f6e21e595df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 00:18:16 +0800 Subject: [PATCH] =?UTF-8?q?task-211:=20ArchUnit=20Controller=20=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E8=A7=84=E5=88=99=EF=BC=88=E4=B8=8D=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=20Mapper/=E5=A4=96=E9=83=A8=20Client?= =?UTF-8?q?=EF=BC=8C=E9=9B=B6=E8=BF=9D=E8=A7=84=EF=BC=89+=20task=E2=86=92?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=20=E4=BE=9D=E8=B5=96=E9=98=B2=E6=81=B6?= =?UTF-8?q?=E5=8C=96=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - archunit-junit5(test) 依赖引入一次到位 - Controller 不访问 ..mapper.. / ..client.. 两条规则全绿(探针确认存量零违规) - task 模块经 *ResultFileJobHandler 等依赖业务 128 处(存量)→ 冻结上限防增长;module 切片存在 collectdata→dedupe→task 环,清零需 Handler 迁移等重构,另行处理 --- backend-java/pom.xml | 6 + .../aiimage/ArchitectureBoundaryTest.java | 108 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java diff --git a/backend-java/pom.xml b/backend-java/pom.xml index fb3ebbc7..f897b13e 100644 --- a/backend-java/pom.xml +++ b/backend-java/pom.xml @@ -150,6 +150,12 @@ spring-boot-starter-test test + + com.tngtech.archunit + archunit-junit5 + 1.3.0 + test + diff --git a/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java b/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java new file mode 100644 index 00000000..423ef498 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/ArchitectureBoundaryTest.java @@ -0,0 +1,108 @@ +package com.nanri.aiimage; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-210/211/212:模块边界 ArchUnit 规则(plan 13)。 + * + * 已通过(零违规):任何 Controller 不得直接访问 Mapper 或外部 Client(211)。 + * 债务防恶化(210/212,探针 2026-09-05):task 模块经 *ResultFileJobHandler 等依赖具体业务 + * Service/Mapper 共 128 处、modules 切片存在 collectdata→dedupe→task 环;清零需把 Handler 实现 + * 迁回业务模块等大重构。本文件冻结边界(不允许新增违规;存量≤128),消除债务留待专门重构。 + */ +class ArchitectureBoundaryTest { + + private static final int TASK_TO_BUSINESS_BASELINE = 128; + + private static volatile JavaClasses cached; + + private static JavaClasses app() { + if (cached == null) { + cached = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPackages("com.nanri.aiimage.modules"); + } + return cached; + } + + @Test + void controllersDoNotAccessMapper() { + noClasses().that().resideInAPackage("..controller..") + .should().dependOnClassesThat().resideInAPackage("..mapper..") + .because("Controller 只经 Service 访问 Mapper(单向分层)") + .check(app()); + } + + @Test + void controllersDoNotDependOnExternalClient() { + noClasses().that().resideInAPackage("..controller..") + .should().dependOnClassesThat().resideInAPackage("..client..") + .because("外部 Client 细节由 Service 层收敛,Controller 不应感知") + .check(app()); + } + + @Test + void boundaryRulesCoverAllControllers() { + long controllers = app().stream() + .filter(c -> c.getPackageName().endsWith(".controller")).count(); + assertTrue(controllers >= 10, "应覆盖全部 Controller,实际 " + controllers); + } + + @Test + void taskToBusinessDependencyDoesNotGrow() { + // 冻结存量(探针 128):不允许 task→业务 依赖继续增加 + int violations = countViolations(taskRule()); + assertTrue(violations >= 1, "存在 task→业务 依赖(Handler SPI 承接点)"); + assertTrue(violations <= TASK_TO_BUSINESS_BASELINE, + "task→业务 依赖不得超过基线 " + TASK_TO_BUSINESS_BASELINE + ",当前 " + violations + + ";新增依赖走 Handler SPI 或先消除存量"); + } + + @Test + void ruleFailuresCarryClearReason() { + // 规则附 because 说明,失败信息可定位(构造性校验:规则对象能生成可读描述) + assertTrue(taskRule().getDescription().contains("no classes that reside in a package"), + "规则描述应可读"); + } + + @Test + void spiHandlersAreTheSanctionedBridge() { + // 允许方向:task 模块仅 *ResultFileJobHandler(04 SPI 实现)可依赖业务 Service; + // 其余 task 类若依赖业务即属增长(由 taskToBusinessDependencyDoesNotGrow 守卫上限)。 + JavaClasses classes = app(); + long handlers = classes.stream() + .filter(c -> c.getSimpleName().endsWith("ResultFileJobHandler")) + .count(); + assertTrue(handlers >= 10, "Handler SPI 实现应在 task 模块注册,实际 " + handlers); + } + + private static com.tngtech.archunit.lang.ArchRule taskRule() { + return noClasses().that().resideInAPackage("..modules.task..") + .should().dependOnClassesThat().resideInAPackage( + "..modules.(similarasin|appearancepatent|deletebrand|publish|brand|collectdata|" + + "shopdatacrawl|imagevideo|shopmatch|productrisk|queryasin|withdraw|patroldelete)..") + .because("公共 task 模块不应依赖具体业务模块"); + } + + private static int countViolations(com.tngtech.archunit.lang.ArchRule rule) { + try { + rule.check(app()); + return 0; + } catch (AssertionError e) { + String msg = e.getMessage(); + long count = msg.lines() + .filter(l -> l.trim().startsWith("Constructor <") + || l.trim().startsWith("Field <") + || l.trim().startsWith("Method <") + || l.trim().startsWith("Class <")) + .count(); + return (int) count; + } + } +}