HTTP 协议本身没有「可选参数」这一说法——可选性来自客户端构造请求或服务端声明参数可缺省时的约定。常见需求是:某些 Query 字段、表单键或 JSON 属性有值才发送,缺省时不出现该键,避免 age=、city=null 之类脏参数。
速览
- GET:
URIBuilder/UriComponentsBuilder;Spring 可用queryParamIfPresent。 - POST 表单:
NameValuePair/MultiValueMap,只 add 需要的键。 - REST JSON:DTO +
@JsonInclude(NON_NULL)(类级或全局配置)。 - Spring 接口:
@RequestParam(required = false);JSON 体缺字段 → null。 - Spring 列表筛选:
@ModelAttributePOJO 绑定 Query;springdoc@ParameterObject用于 OpenAPI 展开文档。 - 声明式客户端:OpenFeign
@SpringQueryMap;Retrofit@Query(null 省略)/@QueryMap(Map 内不得含 null,须先过滤)。 - 原则:REST 优先 JSON;不要手拼 Query;对接前确认 缺省 / null / 空串 三种语义。
1. 概念:可选 ≠ 传 null
| 说法 | 实际含义 |
|---|---|
| Query 可选 | URL 里不出现 &age=18,而不是 &age= |
| JSON 可选 | 请求体里没有 "age" 键,而不是 "age": null |
| 表单可选 | 表单字段列表里不包含该 name |
Optional<T> |
业务层表达「可能没有值」;序列化时仍要配合 NON_NULL 或条件 add |
1.1 缺省、null、空串:三种不同语义
对接 API 或设计接口时,三者常被混用,但含义不同:
| 形态 | GET Query 示例 | JSON 示例 | 常见服务端解读 |
|---|---|---|---|
| 缺省(missing) | 无 &age |
无 "age" 键 |
「未指定」→ 不参与筛选 / 用默认值 |
| 显式 null | 少见(age= 不等价) |
"age": null |
PATCH 场景可能表示「清空字段」 |
| 空串 | city= |
"city": "" |
有的接口当「空值搜索」;有的与 missing 等同 |
Jackson 默认:JSON 里没有的键 → Java 字段 null;无法仅靠 null 区分「未传」与「传了 null」。

2. GET:可选 Query 参数
2.1 手拼 URL(仅参数极少时)
StringBuilder url = new StringBuilder("https://api.example.com/user");
url.append("?name=").append(URLEncoder.encode("Tom", StandardCharsets.UTF_8));
if (age != null) {
url.append("&age=").append(age);
}
if (city != null && !city.isBlank()) {
url.append("&city=").append(URLEncoder.encode(city, StandardCharsets.UTF_8));
}
HttpGet request = new HttpGet(url.toString());
直观,但 encode、首尾 ?/&、空字符串边界容易写错,不推荐作为默认方案。
2.2 Apache HttpClient:URIBuilder(推荐)
URIBuilder builder = new URIBuilder("https://api.example.com/user");
builder.addParameter("name", "Tom");
if (age != null) {
builder.addParameter("age", age.toString());
}
if (city != null && !city.isBlank()) {
builder.addParameter("city", city);
}
HttpGet request = new HttpGet(builder.build());
自动 URL encode,可读性好,Apache HttpClient 项目里很常见。
2.3 Spring:UriComponentsBuilder+RestTemplate/WebClient
推荐写法(按条件追加 Query,避免模板里写死 &age={age} 却在 age 为空时仍占位):
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl("https://api.example.com/user")
.queryParam("name", "Tom");
if (age != null) {
builder.queryParam("age", age);
}
if (city != null && !city.isBlank()) {
builder.queryParam("city", city);
}
String uri = builder.build().encode().toUriString();
ResponseEntity<String> resp = restTemplate.getForEntity(uri, String.class);
Spring 6+ 可用 WebClient 同样链式 queryParam:
WebClient client = WebClient.create("https://api.example.com");
client.get()
.uri(uriBuilder -> {
uriBuilder.path("/user").queryParam("name", "Tom");
if (age != null) uriBuilder.queryParam("age", age);
return uriBuilder.build();
})
.retrieve()
.bodyToMono(String.class);
2.4 Spring:queryParamIfPresent(更 idiomatic)
Spring Framework 5.1+ 提供 queryParamIfPresent,与 Optional 配合,避免手写 if:
String uri = UriComponentsBuilder.fromHttpUrl("https://api.example.com/user")
.queryParam("name", "Tom")
.queryParamIfPresent("age", Optional.ofNullable(age))
.queryParamIfPresent("city",
Optional.ofNullable(city).filter(s -> !s.isBlank()))
.build()
.encode()
.toUriString();
Optional.empty() 时该 Query 键不会出现,语义与条件 queryParam 一致。
2.5 OkHttp
HttpUrl.Builder urlBuilder = Objects.requireNonNull(
HttpUrl.parse("https://api.example.com/user")).newBuilder();
urlBuilder.addQueryParameter("name", "Tom");
if (age != null) {
urlBuilder.addQueryParameter("age", String.valueOf(age));
}
if (city != null && !city.isBlank()) {
urlBuilder.addQueryParameter("city", city);
}
Request request = new Request.Builder().url(urlBuilder.build()).get().build();
OkHttp 自动 encode;addQueryParameter 只在调用时追加,未调用即缺省。
2.6 Java 11+HttpClient(标准库)
URI 仍建议用 UriComponentsBuilder 拼好,再交给标准库客户端:
URI uri = UriComponentsBuilder.fromHttpUrl("https://api.example.com/user")
.queryParam("name", "Tom")
.queryParamIfPresent("age", Optional.ofNullable(age))
.build()
.encode()
.toUri();
HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
3. POST 表单:application/x-www-form-urlencoded
只把需要的键放进实体,不传即无该字段:
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("username", "tom"));
if (email != null && !email.isBlank()) {
params.add(new BasicNameValuePair("email", email));
}
HttpPost post = new HttpPost(url);
post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
Spring RestTemplate / WebClient 侧示例:
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "tom");
if (email != null && !email.isBlank()) {
form.add("email", email);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
restTemplate.postForEntity(
url,
new HttpEntity<>(form, headers),
String.class);
WebClient:BodyInserters.fromFormData(form),同样只 add 有值的键。
4. POST JSON:可选字段(REST 推荐)
REST 接口最常用:请求体 DTO + 序列化时忽略 null。
4.1 定义 DTO(类级@JsonInclude推荐)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDTO {
private String name;
private Integer age; // 可选
private String city; // 可选
// getter / setter
}
类上标注 NON_NULL 比全局改 ObjectMapper 更安全,只影响该 DTO,不波及其他序列化场景。
4.2 Jackson:只序列化非 null 字段
全局配置(Spring Boot application.yml):
spring:
jackson:
default-property-inclusion: non_null
或单次 ObjectMapper(Jackson 2.12+ 推荐 setDefaultPropertyInclusion):
ObjectMapper mapper = new ObjectMapper();
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
// 亦可用 setSerializationInclusion(NON_NULL),仍常见;2.12+ 更推荐显式 setDefaultPropertyInclusion
UserDTO dto = new UserDTO();
dto.setName("Tom");
// age / city 不 set → JSON 里不会出现这两个键
String json = mapper.writeValueAsString(dto);
// {"name":"Tom"}
Spring 客户端直接 POST DTO(由 HttpMessageConverter 序列化,需配合 Jackson 配置或类级 @JsonInclude):
UserDTO dto = new UserDTO();
dto.setName("Tom");
restTemplate.postForEntity(url, dto, String.class); // 或 postForEntity<String>(...)
// WebClient
webClient.post()
.uri("/user")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(dto)
.retrieve()
.bodyToMono(String.class);
4.3 与Optional字段
DTO 字段类型用 Optional<Integer> 时,Jackson 默认行为需额外模块或自定义;工程里更常见的是 包装类型 Integer + 不 set,或 @JsonInclude(NON_EMPTY) 处理空集合,而不是在 DTO 上堆 Optional。
4.4 区分「未传字段」与「显式 null」
| 需求 | 做法 |
|---|---|
| 调用方:有值才出现在 JSON | @JsonInclude(NON_NULL) + 不 set 字段 |
| 服务端:读 JSON 区分 missing / null | @JsonSetter(nulls = Nulls.SKIP)、JsonNode、或 OpenAPI JsonNullable<T>(org.openapitools:jackson-databind-nullable) |
| PATCH 部分更新 | 只提交变更字段的 DTO;或 RFC 7396 JSON Merge Patch(application/merge-patch+json) |
JsonNullable 示例(三态:undefined / null / value):
public class PatchUserDTO {
private JsonNullable<Integer> age = JsonNullable.undefined();
// getter / setter
}
5. Spring 服务端:接收可选参数
写接口(而非调接口)时,用注解声明参数可缺省:
@GetMapping("/user")
public List<User> getUser(
@RequestParam String name,
@RequestParam(required = false) Integer age,
@RequestParam(required = false) String city) {
return userService.find(name, age, city);
}
| 注解组合 | 缺省 Query 时行为 |
|---|---|
required = false,无 defaultValue |
参数为 null(包装类型) |
required = false, defaultValue = "" |
得到 空串(String)或需配合类型转换 |
required = true(默认) |
缺参 → 400 Bad Request |
调用示例:
GET /user?name=tom GET /user?name=tom&age=18 GET /user?name=tom&age=18&city=sh
POST 表单接收可选字段:@ModelAttribute UserForm form,form 里未提交的字段一般为 null;或逐个 @RequestParam(required = false)。
JSON 请求体:@RequestBody UserDTO dto,未传的 JSON 键 → 字段 null;age: 0 与「未传 age」在 Integer 字段上可区分,但「未传」与「传 null」仍见 §4.4。
@RequestParam Optional<Integer> age:Spring MVC 较新版本支持;老项目更常用 required = false + null 判断,兼容性更好。
6. 多值 Query 与声明式客户端
6.1 同一键多次出现(multi-value Query)
筛选条件常需要 tag=java&tag=spring,而不是单个逗号拼接(除非 API 文档规定 tags=java,spring)。
客户端(Apache HttpClient):
URIBuilder builder = new URIBuilder(baseUrl);
builder.addParameter("name", "Tom");
for (String tag : tags) { // tags 非空才循环
builder.addParameter("tag", tag);
}
服务端:
@GetMapping("/user")
public List<User> search(
@RequestParam String name,
@RequestParam(required = false) List<String> tag) {
// /user?name=tom&tag=java&tag=spring → tag = ["java", "spring"]
// 无 tag 参数 → tag 为 null(非空 List)
}
Spring 对重复键绑定为 List / Set;是否与「缺省」区分,要在 Service 层显式判断 tag == null || tag.isEmpty()。
6.2 OpenFeign:@SpringQueryMap
微服务里声明式 HTTP 客户端常用 OpenFeign。Query 对象字段为 null 时,Spring Cloud OpenFeign 默认不生成对应 Query 键(与可选参数需求一致):
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user")
List<User> getUser(@SpringQueryMap UserQuery query);
}
public class UserQuery {
private String name;
private Integer age; // null → 不出现在 URL
private String city;
}
注意:Feign 对 空串 与 null 的处理取决于版本与编码器配置;对接前用日志或单元测试确认生成的 URL。
6.3 Retrofit:@Query/@QueryMap
Android 与不少 Java 移动端/网关项目用 Retrofit 声明 HTTP 接口。可选 Query 的惯用写法有两种。
方式 A:逐个 @Query(null 自动省略)
public interface UserApi {
@GET("user")
Call<List<User>> search(
@Query("name") String name,
@Query("age") Integer age, // null → 不出现在 URL
@Query("city") String city);
}
方式 B:@QueryMap + 按需组 Map
@QueryMap 绑定 Map(如 Map<String, String> / Map<String, Object>)。官方文档写明:Map 本身、键、值均不允许为 null——若 put 了 null value,运行时会报错,不会自动跳过。
因此可选参数必须在组 Map 时自行过滤,只放入非 null 的键值对(与下方示例一致):
public interface UserApi {
@GET("user")
Call<List<User>> search(@QueryMap Map<String, Object> params);
}
// 调用前组 Map:只 put 有值的键(切勿 params.put("age", null))
Map<String, Object> params = new HashMap<>();
params.put("name", "Tom");
if (age != null) {
params.put("age", age);
}
if (city != null && !city.isBlank()) {
params.put("city", city);
}
api.search(params).enqueue(callback);
POJO 字段较多时,先用 Bean 工具或手写方法把非 null 字段转成 Map,再交给 @QueryMap——Retrofit 不会像 Feign 的 @SpringQueryMap 那样直接接受任意 POJO。
与 OkHttp 栈的关系:Retrofit 底层通常走 OkHttp。@Query 参数为 null 时由 Retrofit 省略该 Query;@QueryMap 则要求 Map 在传入前已不含 null,过滤发生在客户端组 Map 阶段。
6.4 Spring 列表筛选:@ModelAttribute与@ParameterObject
列表/搜索接口常有一组可选筛选 Query(name、status、createdAfter…),再加 分页(page、size、sort)。逐个写 @RequestParam 冗长,可用 POJO 一次绑定。
数据绑定(Spring MVC):对非简单类型的 Controller 参数,Spring 默认按 @ModelAttribute 把 Query 绑定到 POJO 字段——未出现在 Query 中的字段为 null,不会参与后续条件拼接。显式标注 @ModelAttribute 更清晰(GraalVM Native 等场景也建议显式写)。
OpenAPI 文档(springdoc-openapi):@ParameterObject(org.springdoc.core.annotations.ParameterObject)不负责绑定,而是让 Swagger UI 把 POJO 展开为多个 Query 参数展示;与 Spring MVC 绑定兼容。自 springdoc v1.6.0 起与 Pageable、@PageableDefault 搭配常见。
@GetMapping("/users")
public Page<User> search(
@ParameterObject @ModelAttribute UserSearchCriteria criteria,
@PageableDefault(size = 20, sort = "id", direction = Sort.Direction.DESC)
Pageable pageable) {
return userService.search(criteria, pageable);
}
未使用 springdoc 时,去掉 @ParameterObject,保留 @ModelAttribute(或依赖 Spring 对复杂类型的隐式 @ModelAttribute)即可。
public class UserSearchCriteria {
private String name; // 未传 → null,不参与 SQL 条件
private Integer age;
private UserStatus status;
private LocalDate createdAfter;
// getter / setter
}
请求示例(只传部分筛选 + 分页):
GET /users?name=tom GET /users?name=tom&status=ACTIVE&page=0&size=10 GET /users?page=1&size=20&sort=createdAt,desc
| 参数来源 | 绑定目标 | 缺省行为 |
|---|---|---|
name、age… |
UserSearchCriteria 字段 |
未出现在 Query → 字段 null |
page、size、sort |
Pageable(Spring Data) |
未传 page → 默认 0;未传 size → @PageableDefault 或全局配置 |
Service 层按 null 决定是否加 WHERE 条件(MyBatis 动态 SQL、Specification、CriteriaBuilder 等):
public Page<User> search(UserSearchCriteria c, Pageable pageable) {
return userRepo.findAll((root, query, cb) -> {
List<Predicate> ps = new ArrayList<>();
if (c.getName() != null) {
ps.add(cb.like(root.get("name"), "%" + c.getName() + "%"));
}
if (c.getAge() != null) {
ps.add(cb.equal(root.get("age"), c.getAge()));
}
return cb.and(ps.toArray(new Predicate[0]));
}, pageable);
}
注意
- 绑定靠 Spring MVC
@ModelAttribute;文档展开靠 springdoc@ParameterObject——二者职责不同。 - POST JSON 仍用
@RequestBodyDTO,不用@ModelAttribute。 - springdoc 的
@ParameterObject不支持嵌套 POJO 展开;复杂筛选宜扁平化字段。 Pageable的sort与自定义筛选字段同名时会冲突,复杂排序建议白名单字段。
7. 场景选型与最佳实践
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| GET 调第三方 API | URIBuilder / UriComponentsBuilder |
自动 encode,条件 addParameter |
| POST 老式表单 | NameValuePair / MultiValueMap |
只 add 存在的键 |
| REST JSON API | DTO + NON_NULL |
最清晰,与 OpenAPI 文档一致 |
| Spring 提供 HTTP 接口 | @RequestParam(required=false) / 可空 DTO 字段 |
与调用方 Query/JSON 约定对齐 |
| OkHttp | HttpUrl.Builder.addQueryParameter |
与 URIBuilder 同思路 |
| Java 11+ HttpClient | UriComponentsBuilder 拼 URI + HttpClient.send |
无第三方依赖 |
| OpenFeign 调下游 | @SpringQueryMap POJO |
null 字段默认省略 Query |
| Retrofit(Android 等) | @Query null 省略 / @QueryMap 先过滤 Map |
Map 内 null 会报错,勿依赖自动跳过 |
| Spring 列表筛选 API | @ModelAttribute POJO + Pageable |
未传 Query 键 → 字段 null;springdoc 加 @ParameterObject 展开文档 |
| 多选筛选 tag | 同键多次 addParameter / List 接收 |
勿假设逗号分隔 unless 文档写明 |

实践要点
- REST 能用 JSON 就别滥用 Query——复杂结构、可选字段多时用 JSON 体更清晰。
- DTO +
@JsonInclude(NON_NULL)控制可选 JSON 字段,避免"field": null污染契约。 - 不要手拼 Query 字符串——encode、
?/&、空值边界易错;Spring 优先queryParamIfPresent。 - 「可选」= 键/参数可不存在,不是强行传
null或空串(除非 API 文档明确要求)。 - RestTemplate URI 模板
?name={name}&age={age}在 age 未放入Map时行为易踩坑,优先 Builder 拼完整 URI。 - 对接前对齐三态语义:missing、null、空串;PATCH 与查询接口规则往往不同。
- 多值 Query 用重复键还是逗号分隔,以 OpenAPI/联调为准,客户端与服务端勿各写一套。
- 列表搜索接口 用
@ModelAttributePOJO 收筛选、Pageable收分页;用 springdoc 时再加@ParameterObject改善 OpenAPI 展示。Service 层对 null 字段不加条件。












