task-211: ArchUnit Controller 边界规则(不直接访问 Mapper/外部 Client,零违规)+ task→业务 依赖防恶化基线

- archunit-junit5(test) 依赖引入一次到位
- Controller 不访问 ..mapper.. / ..client.. 两条规则全绿(探针确认存量零违规)
- task 模块经 *ResultFileJobHandler 等依赖业务 128 处(存量)→ 冻结上限防增长;module 切片存在 collectdata→dedupe→task 环,清零需 Handler 迁移等重构,另行处理
This commit is contained in:
2026-09-05 00:18:16 +08:00
parent 5c63604b5b
commit f5d4410063
2 changed files with 114 additions and 0 deletions
+6
View File
@@ -150,6 +150,12 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -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 或外部 Client211)。
* 债务防恶化(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 模块仅 *ResultFileJobHandler04 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;
}
}
}