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

Spring Boot @PostMapping如何在请求体映射前检测空字段区分两类请求

Spring Boot区分请求体未传字段与显式传null的实现方案

核心原理

Spring Boot默认使用Jackson完成JSON到Java对象的反序列化,只有请求JSON中明确存在的key,才会触发对应字段的赋值逻辑,未出现的key不会对Java对象的对应字段做任何操作,我们可以利用这个特性区分两种场景。


方案1:添加字段存在性标记位(最易实现,无额外依赖)

通过新增标记位记录对应字段的setter是否被调用,实现场景区分:

@Entity
@Table(name="Users")
public class User {
    @Id
    @JsonProperty("USER")
    @Column(name = "USER")
    private String userId;

    @JsonProperty("FIRST_NAME")
    @Column(name = "FIRST_NAME")
    private String firstname;

    @JsonProperty("LAST_NAME")
    @Column(name = "LAST_NAME")
    private String lastname;
    
    @JsonProperty("AGE")
    @Column(name = "AGE")
    private String age;

    // 新增存在性标记,默认false,@Transient避免持久化到数据库
    @Transient
    private boolean lastNamePresent = false;
    @Transient
    private boolean agePresent = false;

    // 重写对应setter,只要调用就标记为字段已传
    @JsonProperty("LAST_NAME")
    public void setLastname(String lastname) {
        this.lastNamePresent = true;
        this.lastname = lastname;
    }

    @JsonProperty("AGE")
    public void setAge(String age) {
        this.agePresent = true;
        this.age = age;
    }

    // 新增判断用getter
    public boolean isLastNamePresent() {
        return lastNamePresent;
    }
    public boolean isAgePresent() {
        return agePresent;
    }

    // 其他原有getter、setter保持不变
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
    public String getFirstname() {
        return firstname;
    }
    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }
    public String getLastname() {
        return lastname;
    }
    public String getAge() {
        return age;
    }
}

Controller层判断逻辑:

@PostMapping("/post")
public ResponseEntity<User> insertFromBody(@Valid @RequestBody User user) {
    if (user.isLastNamePresent()) {
        if (user.getLastname() == null) {
            // 第二种场景:显式传LAST_NAME为null
        }
    } else {
        // 第一种场景:未携带LAST_NAME字段
    }
    // AGE字段判断逻辑同理
    // 调用业务逻辑
}

方案2:使用JDK8+ Optional类型(代码更简洁)

将需要判断的字段定义为Optional类型,Jackson反序列化时会自动区分:未传字段的Optional对象本身为null,显式传null的字段会被序列化为Optional.empty():

@JsonProperty("LAST_NAME")
@Column(name = "LAST_NAME")
private Optional<String> lastname;

@JsonProperty("AGE")
@Column(name = "AGE")
private Optional<String> age;

判断逻辑:

if (user.getLastname() == null) {
    // 第一种场景:未携带LAST_NAME字段
} else if (user.getLastname().isEmpty()) {
    // 第二种场景:显式传LAST_NAME为null
} else {
    // 传了非null的LAST_NAME值
    String lastNameVal = user.getLastname().get();
}

注意:如果需要将Optional字段持久化到数据库,需要JPA 2.2及以上版本支持,低版本JPA需额外配置类型转换器。


方案3:全局请求拦截(无需修改实体类)

如果不能修改User实体类,可以通过RequestBodyAdvice全局拦截请求,提前解析原始JSON判断字段是否存在,存入请求上下文供Controller使用:

@ControllerAdvice
public class UserRequestAdvice implements RequestBodyAdvice {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 只处理参数为User类型的请求
        return User.class.isAssignableFrom(methodParameter.getParameterType());
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        // 读取原始请求体
        String body = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8);
        JsonNode jsonNode = objectMapper.readTree(body);
        // 将字段存在性标记存入请求属性
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            attributes.setAttribute("LAST_NAME_EXIST", jsonNode.has("LAST_NAME"), RequestAttributes.SCOPE_REQUEST);
            attributes.setAttribute("AGE_EXIST", jsonNode.has("AGE"), RequestAttributes.SCOPE_REQUEST);
        }
        // 构造新的请求流返回,不影响后续反序列化
        return new MappingJacksonInputMessage(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)), 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;
    }
}

Controller层取值逻辑:

@PostMapping("/post")
public ResponseEntity<User> insertFromBody(@Valid @RequestBody User user) {
    RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
    boolean lastNameExist = (boolean) attributes.getAttribute("LAST_NAME_EXIST", RequestAttributes.SCOPE_REQUEST);
    boolean ageExist = (boolean) attributes.getAttribute("AGE_EXIST", RequestAttributes.SCOPE_REQUEST);

    if (lastNameExist && user.getLastname() == null) {
        // 第二种场景:显式传LAST_NAME为null
    } else if (!lastNameExist) {
        // 第一种场景:未携带LAST_NAME字段
    }
    // AGE字段判断逻辑同理
}

内容的提问来源于stack exchange,提问作者PrasadM96

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 03:39:00