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

