@WebMvcTest单属性请求始终返回400错误,求排查原因
问题:单个字段请求POJO导致Spring MVC测试返回400 Bad Request,添加无用字段后恢复正常
问题场景
控制器端点代码:
@PostMapping("/{productId}/activation/") public ResponseEntity<ProductResponse> toggleProductActivation(@PathVariable("productId") UUID productId, @RequestBody @Valid ToggleProductActivationRequest toggleProductActivationRequest) throws ApplicationException { var product = toggleProductActivationUseCase.handle(productCommandAssembler.assembleToggleProductActivationCommand( toggleProductActivationRequest, productId)); return ResponseEntity.ok() .body(productResponseAssembler.assembleProductResponse(product)); }
原请求POJO(仅单个字段):
@Builder @Getter public class ToggleProductActivationRequest { @NotNull private Boolean active; }
单元测试代码:
@WebMvcTest(value = ProductAdminController.class) class ProductAdminControllerTest { private static final String EXCEPTION_MESSAGE = "Exception"; @Autowired protected ObjectMapper objectMapper; @Autowired private MockMvc mockMvc; @MockBean private ToggleProductActivationUseCase toggleProductActivationUseCase; @MockBean private ProductCommandAssembler productCommandAssembler; @MockBean private ProductResponseAssembler productResponseAssembler; @Test void whenToggleProductActivation_givenProductDoesNotExist_thenReturnNotFound404() throws Exception { // given var productId = UUID.randomUUID(); var toggleProductActivationRequest = givenToggleProductActivationRequest(); var toggleProductActivationCommand = mock(ToggleProductActivationCommand.class); when(productCommandAssembler.assembleToggleProductActivationCommand(any(ToggleProductActivationRequest.class), eq(productId), eq(null))).thenReturn(toggleProductActivationCommand); when(toggleProductActivationUseCase.handle(toggleProductActivationCommand)).thenThrow(new NotFoundException( EXCEPTION_MESSAGE)); // when & then mockMvc.perform(post("/api/v1.0/admin/products/" + productId.toString() + "/activation/") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(toggleProductActivationRequest))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.httpStatus").value(404)) .andExpect(jsonPath("$.reason").value(EXCEPTION_MESSAGE)); } private ToggleProductActivationRequest givenToggleProductActivationRequest() { return ToggleProductActivationRequest.builder() .active(true) .build(); } }
现象:运行测试时始终返回400 Bad Request,但给POJO添加一个无用的test字段后,测试能正常返回预期的404结果。
原因分析
这是Lombok @Builder与Jackson序列化/反序列化的兼容问题,核心点:
- 当POJO仅含单个字段时,Lombok生成的Builder不会附带无参构造函数,Jackson默认尝试通过构造函数反序列化请求体,但布尔类型字段的参数映射容易出现匹配失败,导致请求体反序列化失败。
@Valid注解触发的参数校验会在业务逻辑执行前就因为反序列化失败返回400,无法进入后续代码抛出NotFoundException。- 添加第二个字段后,Lombok生成的Builder结构发生变化,Jackson可以通过字段映射或Builder方法完成反序列化,绕过了构造函数匹配的问题。
解决方案
无需添加无用字段,以下几种方式可修复:
1. 为POJO添加无参构造函数
配合Lombok的@NoArgsConstructor和@AllArgsConstructor(@Builder需要全参构造函数支持):
@Builder @Getter @NoArgsConstructor @AllArgsConstructor public class ToggleProductActivationRequest { @NotNull private Boolean active; }
2. 指定Jackson使用Builder反序列化
通过@JsonDeserialize明确告诉Jackson使用Lombok生成的Builder类处理反序列化:
@Builder @Getter @JsonDeserialize(builder = ToggleProductActivationRequest.ToggleProductActivationRequestBuilder.class) public class ToggleProductActivationRequest { @NotNull private Boolean active; }
3. 改用@Data注解(若业务允许)
@Data会自动生成无参构造函数和setter方法,Jackson可正常处理单个字段的反序列化:
@Data @Builder public class ToggleProductActivationRequest { @NotNull private Boolean active; }
内容的提问来源于stack exchange,提问作者Web2h
相关产品推荐
相关产品推荐

