Spring Boot中如何使用@Valid注解验证Outgoing(非传入)请求体负载
嘿,我懂你的需求——平时用@Valid校验别人发过来的请求体特别顺手,但现在想在自己的Spring Boot服务当客户端、给外部服务发请求时,也能用这套校验规则来检查自己发出去的请求体,对吧?其实Spring默认确实没帮我们自动处理 outgoing 请求的校验,但实现起来一点都不复杂,我给你捋几个实用的方案:
先做好前置准备:给请求实体加校验注解
不管是入参还是出参,校验规则都是定义在实体类上的,这一步和你平时用@Valid校验入参完全一样。比如你提到的SomeRequest,我们给它加上JSR-380的校验注解(Spring Boot的web starter已经自带了这个依赖,老版本可能要单独加spring-boot-starter-validation):
// 注意:Spring Boot 3.x用jakarta.validation注解,2.x用javax.validation public record SomeRequest( @NotNull(message = "ID不能为空") Long id, @Size(min = 2, max = 20, message = "名称长度得在2到20字符之间") String name ) {}
方案一:手动触发校验(最直接的方式)
这是最容易上手的方法,我们直接用Spring容器里的Validator bean,在发送请求前手动校验请求体。比如在调用外部服务的客户端类里:
@Service public class ExternalApiClient { private final RestTemplate restTemplate; private final Validator validator; // Spring 4.3+支持构造方法自动注入,不用写@Autowired public ExternalApiClient(RestTemplate restTemplate, Validator validator) { this.restTemplate = restTemplate; this.validator = validator; } public String callExternalService(SomeRequest request) { // 先手动校验请求体 Set<ConstraintViolation<SomeRequest>> violations = validator.validate(request); if (!violations.isEmpty()) { // 校验不通过的话,直接抛异常(也可以自己组装错误信息返回) throw new ConstraintViolationException(violations); // 要是想自定义错误提示,也可以这么写: // String errorMsg = violations.stream() // .map(v -> v.getPropertyPath() + ": " + v.getMessage()) // .collect(Collectors.joining(", ")); // throw new IllegalArgumentException("请求参数不合法:" + errorMsg); } // 校验通过后,再给外部服务发请求 String externalUrl = "https://external-service.example.com/api/your-endpoint"; return restTemplate.postForObject(externalUrl, request, String.class); } }
这种方式的好处是灵活,你可以在需要的地方单独触发校验,不用全局配置。
方案二:用WebClient拦截器实现全局自动校验
如果你的项目里主要用WebClient来调用外部服务,那可以搞个全局拦截器,让所有通过WebClient发送的请求自动触发校验,不用每个调用点都写重复的校验代码:
首先写个拦截器类:
@Component public class OutgoingRequestValidationInterceptor implements ExchangeFilterFunction { private final Validator validator; public OutgoingRequestValidationInterceptor(Validator validator) { this.validator = validator; } @Override public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { // 拿到要发送的请求体,先判断是否存在 Object requestBody = request.body(); if (requestBody != null) { Set<ConstraintViolation<Object>> violations = validator.validate(requestBody); if (!violations.isEmpty()) { // 校验不通过就直接抛异常,终止请求发送 return Mono.error(new ConstraintViolationException(violations)); } } // 校验通过,继续发送请求 return next.exchange(request); } }
然后配置WebClient的时候把这个拦截器加进去:
@Configuration public class WebClientConfig { @Bean public WebClient webClient(OutgoingRequestValidationInterceptor validationInterceptor) { return WebClient.builder() .filter(validationInterceptor) .baseUrl("https://external-service.example.com") .build(); } }
这样一来,不管你用这个WebClient发什么POST/PUT请求,只要带了请求体,都会自动触发校验,省心很多。
方案三:用AOP+自定义注解,实现类似@Valid的自动校验
要是你想让出参校验的写法和入参的@Valid一模一样,那可以搞个自定义注解,再配合AOP切面来实现自动校验:
首先定义一个自定义注解:
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ValidOutgoing { }
然后写个AOP切面,拦截所有带这个注解的方法参数:
@Aspect @Component public class OutgoingRequestValidationAspect { private final Validator validator; public OutgoingRequestValidationAspect(Validator validator) { this.validator = validator; } // 切面表达式:拦截所有方法中,参数带有@ValidOutgoing注解的方法 @Before("execution(* *(.., @ValidOutgoing (*), ..))") public void validateOutgoingRequest(JoinPoint joinPoint) { // 遍历方法的所有参数 for (Object arg : joinPoint.getArgs()) { if (arg != null) { // 检查参数是否带有@ValidOutgoing注解 ValidOutgoing annotation = AnnotationUtils.getAnnotation(arg.getClass(), ValidOutgoing.class); if (annotation != null) { Set<ConstraintViolation<Object>> violations = validator.validate(arg); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } } } } } }
现在你调用外部服务的时候,只要在请求体参数上加上@ValidOutgoing注解,就会自动触发校验,和你用@Valid校验入参的体验完全一致:
public String callExternalService(@ValidOutgoing SomeRequest request) { // 这里不用手动写校验代码,切面会自动处理 String externalUrl = "https://external-service.example.com/api/your-endpoint"; return restTemplate.postForObject(externalUrl, request, String.class); }
这样几种方案下来,你可以根据自己项目的实际情况选最合适的——要是只是偶尔调用外部服务,用手动校验就够了;要是大量调用,全局拦截或者AOP的方式会更省心。
备注:内容来源于stack exchange,提问作者PatPanda

