首页 > 编程开发 > Java    日期:2026-07-03 / 浏览

HTTP 协议本身没有「可选参数」这一说法——可选性来自客户端构造请求服务端声明参数可缺省时的约定。常见需求是:某些 Query 字段、表单键或 JSON 属性有值才发送,缺省时不出现该键,避免 age=city=null 之类脏参数。

速览

  • GETURIBuilder / UriComponentsBuilder;Spring 可用 queryParamIfPresent
  • POST 表单NameValuePair / MultiValueMap,只 add 需要的键。
  • REST JSONDTO + @JsonInclude(NON_NULL)(类级或全局配置)。
  • Spring 接口@RequestParam(required = false);JSON 体缺字段 → null
  • Spring 列表筛选@ModelAttribute POJO 绑定 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);

WebClientBodyInserters.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 Patchapplication/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 键 → 字段 nullage: 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

列表/搜索接口常有一组可选筛选 QuerynamestatuscreatedAfter…),再加 分页pagesizesort)。逐个写 @RequestParam 冗长,可用 POJO 一次绑定

数据绑定(Spring MVC):对非简单类型的 Controller 参数,Spring 默认按 @ModelAttribute 把 Query 绑定到 POJO 字段——未出现在 Query 中的字段为 null,不会参与后续条件拼接。显式标注 @ModelAttribute 更清晰(GraalVM Native 等场景也建议显式写)。

OpenAPI 文档(springdoc-openapi)@ParameterObjectorg.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
参数来源 绑定目标 缺省行为
nameage UserSearchCriteria 字段 未出现在 Query → 字段 null
pagesizesort Pageable(Spring Data) 未传 page → 默认 0;未传 size@PageableDefault 或全局配置

Service 层按 null 决定是否加 WHERE 条件(MyBatis 动态 SQL、SpecificationCriteriaBuilder 等):

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 仍用 @RequestBody DTO,不用 @ModelAttribute
  • springdoc 的 @ParameterObject 不支持嵌套 POJO 展开;复杂筛选宜扁平化字段。
  • Pageablesort 与自定义筛选字段同名时会冲突,复杂排序建议白名单字段。

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 文档写明

实践要点

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

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章