Skip to content

Commit 0ff48db

Browse files
CodeCasterXclaude
andcommitted
fix(fit): 修复 validation 守卫漏检约束注解的多个场景
修复 hasConstraintAnnotations 守卫方法在以下场景无法检测约束注解的问题: 1. 仅标注 PARAMETER 目标的约束注解(如自定义 @ParameterOnlyConstraint) 未被 hasConstraintAnnotationsInParameter 检测,因原逻辑仅检查 TYPE_USE 注解。新增 parameter.getAnnotations() 检查。 2. 约束注解仅声明在接口方法参数上时,Java 反射中方法参数注解不从 接口继承,导致守卫检查返回 false。重构方法签名为接受 Method, 遍历接口层次结构查找同签名接口方法的参数注解。 3. javax 版 hasConstraintAnnotationsInType 存在逻辑短路,当参数上 有非约束注解时直接返回 false,跳过泛型类型参数检查。 同时为两个插件添加 validation-api 的 sharedDependencies 配置。 Closes #412 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ef7fe64 commit 0ff48db

File tree

10 files changed

+335
-29
lines changed

10 files changed

+335
-29
lines changed

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
<configuration>
8888
<category>system</category>
8989
<level>4</level>
90+
<sharedDependencies>
91+
<sharedDependency>
92+
<groupId>jakarta.validation</groupId>
93+
<artifactId>jakarta.validation-api</artifactId>
94+
</sharedDependency>
95+
</sharedDependencies>
9096
</configuration>
9197
</plugin>
9298
<plugin>

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
2+
* Copyright (c) 2025-2026 Huawei Technologies Co., Ltd. All rights reserved.
33
* This file is a part of the ModelEngine Project.
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
@@ -24,6 +24,7 @@
2424
import java.lang.annotation.Annotation;
2525
import java.lang.reflect.AnnotatedParameterizedType;
2626
import java.lang.reflect.AnnotatedType;
27+
import java.lang.reflect.Method;
2728
import java.lang.reflect.Parameter;
2829
import java.util.Arrays;
2930
import java.util.Locale;
@@ -72,11 +73,12 @@ public void setLocale(Locale locale) {
7273
*/
7374
@Before(value = "@target(validated) && execution(public * *(..))", argNames = "joinPoint, validated")
7475
private void handle(JoinPoint joinPoint, Validated validated) {
76+
Method method = joinPoint.getMethod();
7577
// 检查方法参数是否包含被 jakarta.validation.Constraint 标注的校验注解。
76-
if (hasJakartaConstraintAnnotations(joinPoint.getMethod().getParameters())) {
78+
if (hasJakartaConstraintAnnotations(method)) {
7779
ExecutableValidator execVal = this.validator.forExecutables();
7880
Set<ConstraintViolation<Object>> result = execVal.validateParameters(joinPoint.getTarget(),
79-
joinPoint.getMethod(),
81+
method,
8082
joinPoint.getArgs(),
8183
validated.value());
8284
if (!result.isEmpty()) {
@@ -94,11 +96,32 @@ public void close() {
9496
/**
9597
* 检查方法参数是否包含 {@code jakarta.validation} 校验注解。
9698
*
97-
* @param parameters 表示可能携带校验注解的方法参数数组 {@link Parameter}{@code []}。
99+
* @param method 表示待检查的方法 {@link Method}。
98100
* @return 如果包含 {@code jakarta.validation} 标注的校验注解则返回 {@code true},否则返回 {@code false}。
99101
*/
100-
private boolean hasJakartaConstraintAnnotations(Parameter[] parameters) {
101-
return Arrays.stream(parameters).anyMatch(this::hasConstraintAnnotationsInParameter);
102+
private boolean hasJakartaConstraintAnnotations(Method method) {
103+
if (Arrays.stream(method.getParameters()).anyMatch(this::hasConstraintAnnotationsInParameter)) {
104+
return true;
105+
}
106+
return this.hasConstraintAnnotationsInInterfaces(method);
107+
}
108+
109+
private boolean hasConstraintAnnotationsInInterfaces(Method method) {
110+
Class<?> clazz = method.getDeclaringClass();
111+
while (clazz != null) {
112+
for (Class<?> iface : clazz.getInterfaces()) {
113+
try {
114+
Method interfaceMethod = iface.getMethod(method.getName(), method.getParameterTypes());
115+
if (Arrays.stream(interfaceMethod.getParameters()).anyMatch(this::hasConstraintAnnotationsInParameter)) {
116+
return true;
117+
}
118+
} catch (NoSuchMethodException ignored) {
119+
// 当前接口未声明该方法,继续检查其他接口。
120+
}
121+
}
122+
clazz = clazz.getSuperclass();
123+
}
124+
return false;
102125
}
103126

104127
/**
@@ -108,6 +131,9 @@ private boolean hasJakartaConstraintAnnotations(Parameter[] parameters) {
108131
* @return 如果包含 {@code jakarta.validation} 标注的校验注解则返回 {@code true},否则返回 {@code false}。
109132
*/
110133
private boolean hasConstraintAnnotationsInParameter(Parameter parameter) {
134+
if (Arrays.stream(parameter.getAnnotations()).anyMatch(this::isJakartaConstraintAnnotation)) {
135+
return true;
136+
}
111137
return hasConstraintAnnotationsInType(parameter.getAnnotatedType());
112138
}
113139

@@ -163,4 +189,4 @@ private boolean isJakartaConstraintAnnotation(Annotation annotation) {
163189
return "jakarta.validation".equals(packageName) && "Constraint".equals(className);
164190
});
165191
}
166-
}
192+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationDataControllerTest.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
2+
* Copyright (c) 2025-2026 Huawei Technologies Co., Ltd. All rights reserved.
33
* This file is a part of the ModelEngine Project.
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
@@ -92,4 +92,24 @@ void shouldFailedWhenCreateInvalidCompanyWithGroup() {
9292
this.response = this.mockMvc.perform(requestBuilder);
9393
assertThat(this.response.statusCode()).isEqualTo(500);
9494
}
95-
}
95+
96+
@Test
97+
@DisplayName("RequestParam 参数 NotBlank 校验 - 空白值应返回 500")
98+
void shouldFailWhenRequestParamIsBlank() {
99+
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/param/notblank")
100+
.param("name", " ")
101+
.responseType(Void.class);
102+
this.response = this.mockMvc.perform(requestBuilder);
103+
assertThat(this.response.statusCode()).isEqualTo(500);
104+
}
105+
106+
@Test
107+
@DisplayName("RequestParam 参数 NotBlank 校验 - 合法值应返回 200")
108+
void shouldOkWhenRequestParamIsNotBlank() {
109+
MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/param/notblank")
110+
.param("name", "validName")
111+
.responseType(Void.class);
112+
this.response = this.mockMvc.perform(requestBuilder);
113+
assertThat(this.response.statusCode()).isEqualTo(200);
114+
}
115+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
2+
* Copyright (c) 2025-2026 Huawei Technologies Co., Ltd. All rights reserved.
33
* This file is a part of the ModelEngine Project.
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
@@ -11,7 +11,11 @@
1111
import static org.mockito.Mockito.mock;
1212
import static org.mockito.Mockito.when;
1313

14+
import jakarta.validation.Constraint;
1415
import jakarta.validation.ConstraintViolationException;
16+
import jakarta.validation.Payload;
17+
import jakarta.validation.Valid;
18+
import jakarta.validation.constraints.NotNull;
1519
import modelengine.fitframework.aop.JoinPoint;
1620
import modelengine.fitframework.ioc.BeanContainer;
1721
import modelengine.fitframework.ioc.annotation.AnnotationMetadataResolver;
@@ -31,6 +35,10 @@
3135
import org.junit.jupiter.api.Test;
3236
import org.mockito.Mockito;
3337

38+
import java.lang.annotation.ElementType;
39+
import java.lang.annotation.Retention;
40+
import java.lang.annotation.RetentionPolicy;
41+
import java.lang.annotation.Target;
3442
import java.lang.reflect.InvocationTargetException;
3543
import java.lang.reflect.Method;
3644
import java.math.BigDecimal;
@@ -65,20 +73,73 @@ void setUp() {
6573
}
6674

6775
private ConstraintViolationException invokeHandleMethod(Method targetMethod, Object[] args) {
76+
return this.invokeHandleMethod(targetMethod, this.validateService, args);
77+
}
78+
79+
private ConstraintViolationException invokeHandleMethod(Method targetMethod, Object target, Object[] args) {
6880
Method handleValidatedMethod =
6981
ReflectionUtils.getDeclaredMethod(ValidationHandler.class, "handle", JoinPoint.class, Validated.class);
7082
handleValidatedMethod.setAccessible(true);
7183
JoinPoint joinPoint = mock(JoinPoint.class);
7284
when(joinPoint.getMethod()).thenReturn(targetMethod);
7385
when(joinPoint.getArgs()).thenReturn(args);
74-
when(joinPoint.getTarget()).thenReturn(this.validateService);
86+
when(joinPoint.getTarget()).thenReturn(target);
7587

7688
InvocationTargetException invocationTargetException = catchThrowableOfType(InvocationTargetException.class,
7789
() -> handleValidatedMethod.invoke(this.handler, joinPoint, this.validated));
7890

7991
return ObjectUtils.cast(invocationTargetException.getTargetException());
8092
}
8193

94+
private boolean invokeHasJakartaConstraintAnnotations(Method targetMethod) {
95+
Method hasJakartaConstraintAnnotationsMethod =
96+
ReflectionUtils.getDeclaredMethod(ValidationHandler.class, "hasJakartaConstraintAnnotations",
97+
Method.class);
98+
hasJakartaConstraintAnnotationsMethod.setAccessible(true);
99+
try {
100+
return ObjectUtils.cast(hasJakartaConstraintAnnotationsMethod.invoke(this.handler, targetMethod));
101+
} catch (IllegalAccessException | InvocationTargetException exception) {
102+
throw new IllegalStateException("调用 hasJakartaConstraintAnnotations 失败", exception);
103+
}
104+
}
105+
106+
@Test
107+
@DisplayName("仅 PARAMETER 目标的约束注解应被检测")
108+
void shouldDetectParameterConstraintWithoutTypeUse() {
109+
Method method =
110+
ReflectionUtils.getDeclaredMethod(GuardValidationService.class, "validateParameterOnly", String.class);
111+
boolean hasConstraintAnnotations = invokeHasJakartaConstraintAnnotations(method);
112+
assertThat(hasConstraintAnnotations).isTrue();
113+
}
114+
115+
@Test
116+
@DisplayName("存在非约束类型注解时仍应检测泛型参数中的约束注解")
117+
void shouldDetectConstraintInGenericTypeWhenTypeHasNonConstraintAnnotation() {
118+
Method method = ReflectionUtils.getDeclaredMethod(GuardValidationService.class, "validateEmployeeList",
119+
List.class);
120+
boolean hasConstraintAnnotations = invokeHasJakartaConstraintAnnotations(method);
121+
assertThat(hasConstraintAnnotations).isTrue();
122+
}
123+
124+
@Test
125+
@DisplayName("接口声明约束注解时实现类方法也应被检测")
126+
void shouldDetectConstraintAnnotationsFromInterface() {
127+
Method method =
128+
ReflectionUtils.getDeclaredMethod(ValidatedCommandHandlerImpl.class, "handle", Employee.class);
129+
boolean hasConstraintAnnotations = invokeHasJakartaConstraintAnnotations(method);
130+
assertThat(hasConstraintAnnotations).isTrue();
131+
}
132+
133+
@Test
134+
@DisplayName("接口声明约束注解时校验应生效")
135+
void shouldValidateWhenConstraintAnnotationsOnInterface() {
136+
Method method =
137+
ReflectionUtils.getDeclaredMethod(ValidatedCommandHandlerImpl.class, "handle", Employee.class);
138+
ConstraintViolationException exception =
139+
invokeHandleMethod(method, new ValidatedCommandHandlerImpl(), new Object[] {null});
140+
assertThat(exception.getMessage()).contains("Command cannot be null.");
141+
}
142+
82143
@Test
83144
@DisplayName("测试校验原始类型成功")
84145
void givePrimitiveThenValidateOk() {
@@ -773,4 +834,34 @@ void testNullValidation() {
773834
assertThat(exception.getMessage()).isNotNull();
774835
}
775836
}
776-
}
837+
838+
private static class GuardValidationService {
839+
public void validateParameterOnly(@ParameterOnlyConstraint String name) {}
840+
841+
public void validateEmployeeList(@NoConstraintTypeUse List<@Valid Employee> employees) {}
842+
}
843+
844+
private interface ValidatedCommandHandler {
845+
void handle(@Valid @NotNull(message = "Command cannot be null.") Employee employee);
846+
}
847+
848+
private static class ValidatedCommandHandlerImpl implements ValidatedCommandHandler {
849+
@Override
850+
public void handle(Employee employee) {}
851+
}
852+
853+
@Target(ElementType.PARAMETER)
854+
@Retention(RetentionPolicy.RUNTIME)
855+
@Constraint(validatedBy = {})
856+
private @interface ParameterOnlyConstraint {
857+
String message() default "参数不合法";
858+
859+
Class<?>[] groups() default {};
860+
861+
Class<? extends Payload>[] payload() default {};
862+
}
863+
864+
@Target(ElementType.TYPE_USE)
865+
@Retention(RetentionPolicy.RUNTIME)
866+
private @interface NoConstraintTypeUse {}
867+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/ValidationDataController.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
2+
* Copyright (c) 2025-2026 Huawei Technologies Co., Ltd. All rights reserved.
33
* This file is a part of the ModelEngine Project.
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

77
package modelengine.fitframework.validation.data;
88

99
import jakarta.validation.Valid;
10+
import jakarta.validation.constraints.NotBlank;
1011
import modelengine.fit.http.annotation.PostMapping;
1112
import modelengine.fit.http.annotation.RequestBody;
1213
import modelengine.fit.http.annotation.RequestMapping;
14+
import modelengine.fit.http.annotation.RequestParam;
1315
import modelengine.fitframework.annotation.Component;
1416
import modelengine.fitframework.validation.Validated;
1517

@@ -38,4 +40,12 @@ public void validateCompanyDefaultGroup(@RequestBody @Valid Company company) {}
3840
*/
3941
@PostMapping(path = "/company/companyGroup", description = "验证 Company 类特定分组注解")
4042
public void validateCompanyGroup(@RequestBody @Valid Company company) {}
41-
}
43+
44+
/**
45+
* 验证 RequestParam 参数的 NotBlank 约束。
46+
*
47+
* @param name 表示待校验的名字参数的 {@link String}。
48+
*/
49+
@PostMapping(path = "/param/notblank", description = "验证 RequestParam 参数的 NotBlank 约束")
50+
public void validateRequestParamNotBlank(@RequestParam("name") @NotBlank String name) {}
51+
}

framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
<configuration>
8888
<category>system</category>
8989
<level>4</level>
90+
<sharedDependencies>
91+
<sharedDependency>
92+
<groupId>jakarta.validation</groupId>
93+
<artifactId>jakarta.validation-api</artifactId>
94+
</sharedDependency>
95+
</sharedDependencies>
9096
</configuration>
9197
</plugin>
9298
<plugin>

0 commit comments

Comments
 (0)