如何使用javax.validation验证路径id与请求体对象id的一致性?
基于javax.validation实现路径参数与请求体ID一致性验证
当然可以用javax.validation框架实现这个需求,核心思路是自定义方法级别的约束注解,用来校验方法中路径参数id和请求体QdCFPresenter的id是否相等。以下是具体实现步骤:
1. 自定义约束注解
创建一个方法级的约束注解,用于标记需要校验的方法,并指定要对比的参数/字段名称:
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PathIdMatchesBodyIdValidator.class) public @interface PathIdMatchesBodyId { String message() default "路径ID与请求体ID不匹配"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String pathIdParam() default "id"; String bodyParam() default "qdcf"; String bodyIdField() default "id"; }
2. 实现约束校验器
编写对应的校验器类,实现ConstraintValidator接口,通过反射获取参数值并完成对比:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class PathIdMatchesBodyIdValidator implements ConstraintValidator<PathIdMatchesBodyId, Object> { private String pathIdParam; private String bodyParam; private String bodyIdField; private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @Override public void initialize(PathIdMatchesBodyId constraintAnnotation) { this.pathIdParam = constraintAnnotation.pathIdParam(); this.bodyParam = constraintAnnotation.bodyParam(); this.bodyIdField = constraintAnnotation.bodyIdField(); } @Override public boolean isValid(Object target, ConstraintValidatorContext context) { // 从Spring的方法验证上下文获取当前方法和参数 org.springframework.validation.beanvalidation.SpringValidatorAdapter.MethodValidationContext validationContext = (org.springframework.validation.beanvalidation.SpringValidatorAdapter.MethodValidationContext) target; Method method = validationContext.getMethod(); Object[] args = validationContext.getArgs(); // 构建参数名与参数值的映射 String[] paramNames = parameterNameDiscoverer.getParameterNames(method); Map<String, Object> paramMap = new HashMap<>(); for (int i = 0; i < paramNames.length; i++) { paramMap.put(paramNames[i], args[i]); } // 获取路径ID和请求体对象 Long pathId = (Long) paramMap.get(pathIdParam); Object bodyObj = paramMap.get(bodyParam); // 若任一参数为空,直接返回true(非空校验由@NotNull单独处理) if (pathId == null || bodyObj == null) { return true; } // 反射获取请求体中的ID字段值并对比 try { Field idField = bodyObj.getClass().getDeclaredField(bodyIdField); idField.setAccessible(true); Long bodyId = (Long) idField.get(bodyObj); return pathId.equals(bodyId); } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalArgumentException("请求体中不存在指定的ID字段", e); } } }
3. 在控制器中应用校验
给控制器类添加@Validated注解开启方法级验证,再在目标方法上添加自定义的@PathIdMatchesBodyId注解:
import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.NotNull; import reactor.core.publisher.Mono; @RestController @Validated // 开启方法级javax.validation验证 public class QdCFController { private final QdCFService qdcfService; public QdCFController(QdCFService qdcfService) { this.qdcfService = qdcfService; } @PutMapping("{id}") @PathIdMatchesBodyId // 应用自定义校验规则 public Mono<QdCFPresenter> save( @PathVariable Long id, @RequestBody @Valid @NotNull QdCFPresenter qdcf ) { return this.qdcfService.store(qdcf); } }
4. 全局处理校验异常
Spring WebFlux中方法级验证失败会抛出ConstraintViolationException,可以全局捕获并返回统一错误响应:
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<String> handleConstraintViolation(ConstraintViolationException e) { String errorMsg = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")); return new ResponseEntity<>(errorMsg, HttpStatus.BAD_REQUEST); } }
补充说明
- 自定义注解的参数可灵活调整,若参数名或字段名不同,只需在注解上指定对应值即可。
- 校验器默认忽略参数为空的情况,因为
@NotNull已单独处理非空校验,若路径参数id必填,也可给其添加@NotNull注解。
内容的提问来源于stack exchange,提问作者Jordi
相关产品推荐
相关产品推荐

