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:
@@ -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 或外部 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user