复杂查询该放 body 了,HTTP QUERY 专管这件事

几乎每个做过搜索接口的后端,都写过这种端点:

1
POST /products/search

创建产品?没有。

为啥还用 POST?筛选条件嵌套三层,品牌数组塞几十个,价格区间再加排序和关键词。GET 的 query string 快被你写成方言了:URI 太长,编码难看,日志还把用户输入整行摊开。大家就默契地用 POST 顶着。

能跑。语义上却一直拧巴。

2026 年 6 月,IETF 把这窟窿补上了。RFC 10008 定了 HTTP 方法 QUERY:条件放 body,方法本身安全、幂等,规范层面也允许缓存。

我习惯叫它「带 body 的 GET 请求」。

GET 不够用,POST 又太重

GET 对“读”是对的。安全、幂等。缓存、代理、可观测性工具最吃这一套。

麻烦在输入塞 URI:

1
GET /products?category=hardware&brand=a&brand=b&price.min=10&price.max=500&sort=-createdAt

简单过滤没问题。一上嵌套对象、布尔组合、大 ID 列表、搜索 DSL,query string 就发明规矩:重复 key、逗号分隔、方括号。

POST 把 body 问题抹平了,语义却写歪了:

1
2
3
4
5
6
7
8
9
POST /products/search HTTP/1.1
Content-Type: application/json

{
"category": "hardware",
"brands": ["a", "b"],
"price": { "min": 10, "max": 500 },
"sort": "-createdAt"
}

你的 Controller 清楚:只读。网关、CDN、客户端重试策略不看你源码。它们只看见 POST。默认不当 GET 缓存,断线也不敢随便重放,生怕真改了状态。

RFC 还提了一件脏事:URI 比 body 更容易进日志、进书签。检索条件挂在 URL 上,泄露面更大。body 也不是自动隐私,只是没那么常被“整行记下来”。

缺口其实很窄:

HTTP 需要一种安全且幂等的方法,把查询输入放在请求内容里,而不是 URI 里。

QUERY 干的就是这个。不是再造一个读动词,是把复杂查询从 GET 和 POST 夹缝里拎出来。

RFC 10008: QUERY 方法

官方抽象很直白:QUERY 请求目标资源,以安全、幂等的方式处理请求体里的内容,再返回结果。

长这样:

1
2
3
4
5
6
7
8
9
10
11
QUERY /products HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
"category": "hardware",
"brands": ["a", "b"],
"price": { "min": 10, "max": 500 },
"sort": "-createdAt"
}

路径圈定资源范围;“怎么查”在 body 和 media type 里。

对照着看:

GET QUERY POST
安全 未必
幂等 未必
查询输入 URI 请求体 请求体(方法语义却是“可能改状态”)
默认可缓存 是(缓存键要算上 body) 通常否

Spring Boot 如何使用

Spring Boot 4.1.0 内置的 Tomcat 解析 HTTP/1.1 时,不必先在 enum 里登记 QUERY 才能进 Servlet。请求能到应用,卡在 Spring 怎么映射。

HttpMethod 已经认 QUERY。Framework 7 里它是 final class,不是死 enum:

1
HttpMethod.valueOf("QUERY")

RestClient、WebClient、声明式 HTTP 客户端发 QUERY,不必干等新常量。

真正卡住的是 MVC 的 RequestMethod。注解路由还绑着:

1
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

于是这行现在编不过:

1
@RequestMapping(method = RequestMethod.QUERY) // 没有 QUERY

WebFlux 函数式路由

method 谓词吃 HttpMethod,不绑 RequestMethod enum。Boot 4.1 可以直接写:

1
2
3
4
5
6
7
8
9
RouterFunction<ServerResponse> routes = RouterFunctions.route()
.route(
RequestPredicates.method(HttpMethod.valueOf("QUERY"))
.and(RequestPredicates.path("/api/items")),
request -> request.bodyToMono(SearchCriteria.class)
.flatMap(criteria ->
ServerResponse.ok().bodyValue(itemService.search(criteria)))
)
.build();

已经在 WebFlux 的项目,改动面小,语义也干净。

自定义 MVC 桥接

注解 Controller 还得等官方扩枚举。在那之前,社区绕开 @RequestMapping(method = …),常见四块:

  1. 方法注解 @HttpQueryMapping(别合成 @RequestMapping,否则又撞 enum)
  2. RequestCondition:运行时比 "QUERY".equalsIgnoreCase(request.getMethod())
  3. 自定义 RequestMappingHandlerMapping:扫到注解就拼 path / consumes / produces + 自定义 condition
  4. 可选:DispatcherServlet 子类 + WebMvcRegistrations,把 QUERY 送进 Spring 分发链

业务侧会长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/api/items")
public class ItemSearchController {

@HttpQueryMapping(
value = "/search",
consumes = "application/json",
produces = "application/json"
)
public List<Item> search(@RequestBody SearchCriteria criteria) {
return itemRepository.findMatching(criteria);
}
}

收尾

截止本文发布,Spring 官方还没把 QUERY 映射到 @RequestMapping 注解里。社区已经有 PR:

spring-framework PR #34993

预计在 spring-framework 7.1.x / Spring Boot 4.2.x 里正式支持。