Spring Boot REST API:请求体JSON转模型前的Schema验证实现
在Spring Boot中用everit-org/json-schema提前验证请求体
我之前也碰到过这个问题,HandlerMethodArgumentResolver确实不太适合在请求体转模型前做验证——毕竟它介入的时候,请求体可能已经被Spring的解析流程读取过了。更靠谱的方式是用RequestBodyAdvice,它是Spring专门为@RequestBody的解析流程设计的扩展点,能在JSON转成模型对象之前拦截并验证请求体。下面是具体的实现步骤:
1. 引入依赖
首先在你的pom.xml(Maven)或者build.gradle里加入everit-org/json-schema和Jackson的依赖:
<!-- everit JSON Schema 依赖 --> <dependency> <groupId>org.everit.json</groupId> <artifactId>org.everit.json.schema</artifactId> <version>1.14.0</version> </dependency> <!-- Jackson 用于JSON处理 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
2. 创建自定义注解标记需要验证的参数
我们需要一个注解来标记哪些@RequestBody参数需要做Schema验证,同时指定对应的Schema文件路径:
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ValidateJsonSchema { // 指定JSON Schema文件的类路径,比如 classpath:schema/user-create.json String value(); }
3. 实现RequestBodyAdvice进行验证
编写一个类实现RequestBodyAdvice,并用@ControllerAdvice注解让Spring识别它。这个类会在请求体被解析成模型前拦截,读取请求体内容并做Schema验证:
@ControllerAdvice public class JsonSchemaValidationAdvice implements RequestBodyAdvice { private final ObjectMapper objectMapper; // 注入Spring默认的ObjectMapper public JsonSchemaValidationAdvice(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 只对标记了@ValidateJsonSchema的@RequestBody参数生效 return methodParameter.hasParameterAnnotation(RequestBody.class) && methodParameter.hasParameterAnnotation(ValidateJsonSchema.class); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { // 1. 读取请求体内容(此时流还没被Spring解析,所以可以安全读取) byte[] bodyBytes = StreamUtils.copyToByteArray(inputMessage.getBody()); String requestBody = new String(bodyBytes, StandardCharsets.UTF_8); // 2. 获取注解中指定的Schema路径并加载Schema ValidateJsonSchema schemaAnnotation = parameter.getParameterAnnotation(ValidateJsonSchema.class); String schemaPath = schemaAnnotation.value(); InputStream schemaStream = getClass().getResourceAsStream(schemaPath); if (schemaStream == null) { throw new IllegalArgumentException("找不到指定的JSON Schema文件: " + schemaPath); } JSONObject schemaJson = new JSONObject(new JSONTokener(schemaStream)); Schema schema = SchemaLoader.load(schemaJson); // 3. 执行验证 try { JSONObject requestJson = new JSONObject(requestBody); schema.validate(requestJson); } catch (ValidationException e) { // 验证失败时抛出自定义异常 throw new JsonSchemaValidationException("请求体不符合Schema规则: " + e.getMessage(), e); } // 4. 重新包装输入流,让后续的Spring解析流程能正常读取请求体 return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(bodyBytes); } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 解析完成后不需要额外处理,直接返回原对象 return body; } @Override public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 空请求体的处理逻辑,可根据需求调整 return body; } }
4. 自定义异常与全局异常处理
创建一个自定义异常类来标识Schema验证失败的情况,再通过全局异常处理器返回友好的错误响应:
// 自定义异常 public class JsonSchemaValidationException extends RuntimeException { public JsonSchemaValidationException(String message, Throwable cause) { super(message, cause); } } // 全局异常处理器 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(JsonSchemaValidationException.class) public ResponseEntity<ErrorResponse> handleSchemaValidationError(JsonSchemaValidationException e) { ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage()); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } // 错误响应模型 public static class ErrorResponse { private int status; private String message; public ErrorResponse(int status, String message) { this.status = status; this.message = message; } // Getter和Setter省略 } }
5. 在Controller中使用
现在你可以在需要验证的@RequestBody参数上标记自定义注解,指定对应的Schema文件:
@RestController @RequestMapping("/users") public class UserController { @PostMapping public ResponseEntity<User> createUser( // 指定要使用的Schema文件 @ValidateJsonSchema("classpath:schema/user-create.json") @RequestBody User user) { // 验证通过后,正常执行业务逻辑 return ResponseEntity.ok(user); } }
备选方案:使用Filter全局验证
如果你需要对多个接口批量验证(比如根据请求路径匹配Schema),可以用Filter实现:
@Component public class JsonSchemaValidationFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; public JsonSchemaValidationFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理POST/PUT等带请求体的请求 if (HttpMethod.POST.matches(request.getMethod()) || HttpMethod.PUT.matches(request.getMethod())) { // 缓存请求体(因为输入流只能读取一次) ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(request); byte[] bodyBytes = cachedRequest.getContentAsByteArray(); String requestBody = new String(bodyBytes, StandardCharsets.UTF_8); // 根据请求路径获取对应的Schema路径 String schemaPath = getSchemaPathByUri(request.getRequestURI()); if (schemaPath != null) { // 加载并验证Schema InputStream schemaStream = getClass().getResourceAsStream(schemaPath); JSONObject schemaJson = new JSONObject(new JSONTokener(schemaStream)); Schema schema = SchemaLoader.load(schemaJson); try { JSONObject requestJson = new JSONObject(requestBody); schema.validate(requestJson); } catch (ValidationException e) { // 验证失败时直接返回错误响应 response.setStatus(HttpStatus.BAD_REQUEST.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "请求体格式错误: " + e.getMessage()); objectMapper.writeValue(response.getWriter(), error); return; } } // 验证通过,继续执行后续流程 filterChain.doFilter(cachedRequest, response); } else { filterChain.doFilter(request, response); } } // 自定义请求路径与Schema的映射逻辑 private String getSchemaPathByUri(String uri) { if ("/users".equals(uri)) { return "classpath:schema/user-create.json"; } // 其他路径的映射规则 return null; } }
两种方案对比:
- RequestBodyAdvice:更精准,针对单个@RequestBody参数,适合不同接口使用不同Schema的场景。
- Filter:适合全局批量验证,根据请求路径统一匹配Schema的场景。
内容的提问来源于stack exchange,提问作者Vinni
相关产品推荐
相关产品推荐

