You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:57:34