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

Spring Boot含默认构造函数时@NonNull/@NotNull注解失效问题

背景

我定义了如下数据类:

@Data
public class Data {
    @lombok.NonNull
    private String name;
}

以及对应的控制器:

@MessageMapping("/data")
public void handleData(@Validated Data data) throws Exception {
    if (data.getName().compareTo("Alice") == 0) {
        logger.info("Alice is here!");
    }
}

同时配置了一个Jackson的ObjectMapper Bean,用于将布尔值转换为整数(True -> 1,False -> 0):

@Bean
ObjectMapper registerObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();

    SimpleModule module = new SimpleModule("MyBoolSerializer");
    module.addSerializer(Boolean.class, new MyBoolSerializer());
    module.addDeserializer(Boolean.class, new MyBoolDeserializer());
    module.addSerializer(boolean.class, new MyBoolSerializer());
    module.addDeserializer(boolean.class, new MyBoolDeserializer());
    
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

    return mapper;
}

当我向/data接口发送未携带name参数的请求时,Jackson会将name设为null,但此时出现如下异常(已解包):

org.springframework.messaging.converter.MessageConversionException: 
Could not read JSON: Cannot construct instance of `com.example.myapp.entity.Data`
(no Creators, like default constructor, exist): 
cannot deserialize from Object value (no delegate- or property-based Creator)
尝试的解决方法

于是我为Data类添加了@NoArgsConstructor注解:

@Data
@NoArgsConstructor   // <<<<
public class Data {
    @lombok.NonNull
    private String name;
}

现在发送请求会触发NullPointerException,参数未被空值校验,代码中的if-statement会被执行。

我尝试改用hibernate-validator的@NotNull注解标记name属性,但结果相同,仍会出现NPE。

问题

我原本认为@NonNull和@NotNull注解可以帮助自动校验数据,无需在控制器中手动校验(如空值检查、范围检查等),但似乎只有在没有默认构造函数时才生效。这逻辑上说得通,因为默认构造函数中没有可校验的数据。

但这与我之前遇到的异常情况相矛盾。

补充信息

我启用了amqp,并配置了自己的MessageConverter Bean,返回一个新的Jackson2JsonMessageConverter实例:

import org.springframework.amqp.support.converter.MessageConverter;
@Bean
MessageConverter jsonMessageConverter() {
    return new Jackson2JsonMessageConverter();
}

此外,项目的pom.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.7</version>
        <relativePath />
        <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo2</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>18</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>8.0.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.4.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-reactor-netty</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

请问该如何解决这个矛盾的问题?


解决方案

1. 修复依赖版本冲突

Spring Boot 2.7.x基于javax.validation规范,你引入的spring-boot-starter-validation:3.0.2和hibernate-validator:8.0.0.Final是基于jakarta.validation的新版本,两者完全不兼容,直接导致校验注解失效。修改pom.xml:

  • 移除spring-boot-starter-validation的手动版本号,使用父pom管理的兼容版本
  • 移除单独的hibernate-validator依赖,spring-boot-starter-validation已包含适配的版本

修改后的依赖片段:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. 让Jackson使用自定义ObjectMapper并支持参数化构造

你自定义的ObjectMapper没有被Jackson2JsonMessageConverter使用,导致反序列化逻辑不符合预期。调整配置:

优化ObjectMapper配置

开启Jackson对参数化构造函数的支持,无需默认构造函数即可反序列化:

@Bean
ObjectMapper registerObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    // 支持通过参数名解析构造函数
    mapper.registerModule(new ParameterNamesModule());
    // 忽略大小写匹配属性
    mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);

    SimpleModule module = new SimpleModule("MyBoolSerializer");
    module.addSerializer(Boolean.class, new MyBoolSerializer());
    module.addDeserializer(Boolean.class, new MyBoolDeserializer());
    module.addSerializer(boolean.class, new MyBoolSerializer());
    module.addDeserializer(boolean.class, new MyBoolDeserializer());
    
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    mapper.registerModule(module);

    return mapper;
}

绑定自定义ObjectMapper到MessageConverter

确保消息转换器使用你的自定义ObjectMapper:

@Bean
MessageConverter jsonMessageConverter(ObjectMapper objectMapper) {
    return new Jackson2JsonMessageConverter(objectMapper);
}

3. 调整Data类注解,确保校验生效

移除@NoArgsConstructor,改用@AllArgsConstructor让Jackson通过参数化构造创建对象,同时使用JSR380的@NotNull做运行时校验:

@Data
@AllArgsConstructor
public class Data {
    @NotNull(message = "name不能为空")
    private String name;
}
  • @AllArgsConstructor避免默认构造函数导致的空值直接赋值
  • @NotNull是运行时校验注解,配合@Validated会在对象绑定后触发校验

4. 启用消息处理的方法校验

添加MethodValidationPostProcessor Bean,确保@MessageMapping方法的@Validated注解生效:

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    return new MethodValidationPostProcessor();
}

效果验证

现在发送未携带name的请求时,会触发ConstraintViolationException,不会进入方法执行,也就不会出现NPE;同时Jackson可以正常反序列化合法请求,布尔值转换逻辑也会生效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 09:20:22