From bcf66dc1d79cd2bd397926a4a8360a74482f919f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 14 Sep 2026 10:37:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(startup):=20=E4=B8=A4=E4=B8=AA=20GroupDelet?= =?UTF-8?q?ionGuard=20=E6=98=BE=E5=BC=8F=E5=91=BD=E5=90=8D=20+=20Bean=20?= =?UTF-8?q?=E5=90=8D=E5=94=AF=E4=B8=80=E6=80=A7=E5=AE=88=E5=8D=AB=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 事故:2026-09-14 边界收敛新增 invalidasin/dedupe 两个同名 GroupDeletionGuard, Spring 默认用简单类名做 Bean 名 → 启动抛 ConflictingBeanDefinitionException, 主机 A 的 java-server 连续重启失败(health 不通),**单测全绿也发现不了** (不起完整 Spring 上下文),部署后才知道。 - 两个守卫分别显式命名 @Service("invalidAsinGroupDeletionGuard") / ("dedupeGroupDeletionGuard") - 新增 SpringComponentBeanNameUniquenessTest:扫描 main 源码,同简单名的组件必须显式命名, 否则红测试(把这次事故固化成可回归的守卫) - 已重新打包部署:JAR 1f1e82bda227f2b0768574f8fd7c32c9,双节点 health=200 --- .../dedupe/service/GroupDeletionGuard.java | 2 +- .../service/GroupDeletionGuard.java | 2 +- ...SpringComponentBeanNameUniquenessTest.java | 89 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/SpringComponentBeanNameUniquenessTest.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/GroupDeletionGuard.java b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/GroupDeletionGuard.java index e0caf129..6112b84d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/GroupDeletionGuard.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/dedupe/service/GroupDeletionGuard.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; /** * 分组删除守卫:去重总数据(2026-09 从 shopkey 收回本模块,文案保持不变)。 */ -@Service +@Service("dedupeGroupDeletionGuard") @RequiredArgsConstructor public class GroupDeletionGuard implements GroupDeletionGuardPort { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/GroupDeletionGuard.java b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/GroupDeletionGuard.java index eef76146..158cf8eb 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/GroupDeletionGuard.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/invalidasin/service/GroupDeletionGuard.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service; /** * 分组删除守卫:品牌数据库(不符合 ASIN)数据(2026-09 从 shopkey 收回本模块,文案保持不变)。 */ -@Service +@Service("invalidAsinGroupDeletionGuard") @RequiredArgsConstructor public class GroupDeletionGuard implements GroupDeletionGuardPort { diff --git a/backend-java/src/test/java/com/nanri/aiimage/SpringComponentBeanNameUniquenessTest.java b/backend-java/src/test/java/com/nanri/aiimage/SpringComponentBeanNameUniquenessTest.java new file mode 100644 index 00000000..6353fb5c --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/SpringComponentBeanNameUniquenessTest.java @@ -0,0 +1,89 @@ +package com.nanri.aiimage; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Spring 组件默认 Bean 名(简单类名)唯一性守卫。 + * + *
2026-09-14 事故:把共享类下沉到 common 时新增了两个同名 {@code GroupDeletionGuard} + * (invalidasin / dedupe 各一),Spring 默认用简单类名做 Bean 名 → 启动直接抛 + * ConflictingBeanDefinitionException,**而单测不起完整上下文,全绿也发现不了**, + * 直到部署后线上 health 不通才暴露(主机 A 连续重启失败)。 + * + *
本测试扫描 main 源码:若两个 Spring 组件简单名相同,则**两者都必须**显式命名
+ * ({@code @Service("xxx")} / {@code @Component("xxx")} 等)才允许;否则红。
+ */
+class SpringComponentBeanNameUniquenessTest {
+
+ private static final Pattern COMPONENT_ANNOTATION = Pattern.compile(
+ "^@(Service|Component|Repository|Controller|RestController|Configuration)\\b(.*)$",
+ Pattern.MULTILINE);
+ private static final Pattern EXPLICIT_NAME = Pattern.compile("^\\s*\\(\\s*\"[^\"]+\"");
+ private static final Pattern CLASS_DECLARATION = Pattern.compile("^public\\s+(?:final\\s+)?class\\s+(\\w+)",
+ Pattern.MULTILINE);
+
+ private record Component(String simpleName, String path, boolean explicitBeanName) {
+ }
+
+ @Test
+ void duplicateComponentSimpleNamesMustBeExplicitlyNamed() throws IOException {
+ Map