如何使用WebTestClient模拟实体中的FilePart类型字段
Spring WebTestClient 上传FilePart时类型不匹配导致400错误
错误现象
使用MultipartBodyBuilder构造请求测试Spring控制器的POST接口时,返回400 BAD_REQUEST,请求响应详情如下:
> POST /customers > WebTestClient-Request-Id: [1] > Content-Type: [multipart/form-data;boundary=x8QNWe6JiRUfEdfbvC6KSeF7RgKVGl6ughbQcs-t] > Accept: [application/json] 1309 bytes of content. < 400 BAD_REQUEST Bad Request < Vary: [Origin, Access-Control-Request-Method, Access-Control-Request-Headers] < Cache-Control: [no-cache, no-store, max-age=0, must-revalidate] < Pragma: [no-cache] < Expires: [0] < X-Content-Type-Options: [nosniff] < X-Frame-Options: [DENY] < X-XSS-Protection: [1 ; mode=block] < Referrer-Policy: [no-referrer] 0 bytes of content (unknown content-type). java.lang.AssertionError: Status expected:<200 OK> but was:<400 BAD_REQUEST> Expected :200 OK Actual :400 BAD_REQUEST
深层错误信息
org.springframework.web.bind.support.WebExchangeBindException: Validation failed for argument at index 0 in method: public reactor.core.publisher.Mono<org.springframework.http.ResponseEntity<nz.co.jware.domain.customer.responses.CustomerResponse>> nz.co.jware.controllers.CustomerController.create(reactor.core.publisher.Mono<nz.co.jware.domain.customer.requests.CustomerRequest>), with 1 error(s): [Field error in object 'customerRequestMono' on field 'logo': rejected value [name]; codes [typeMismatch.customerRequestMono.logo,typeMismatch.logo,typeMismatch.org.springframework.http.codec.multipart.FilePart,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerRequestMono.logo,logo]; arguments []; default message [logo]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'org.springframework.http.codec.multipart.FilePart' for property 'logo'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'org.springframework.http.codec.multipart.FilePart' for property 'logo': no matching editors or conversion strategy found]] at org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver.lambda$null$3(ModelAttributeMethodArgumentResolver.java:134) Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
相关代码
Customer实体
public class Customer { @Id private String id; @NotNull private String company_id; @NotBlank @Size(max = 255) @UniqueElements private String name; @Size(max = 255) private String client_no; @Size(max = 255) private String phone_number; @Size(max = 255) private String logo; @NotNull @JsonProperty("is_activated") private Boolean is_activated; @NotNull private String created_by; @NotNull private String updated_by; @NotNull private LocalDateTime created_at; @NotNull private LocalDateTime updated_at; private List<Location> locations = List.of(); private List<Charge> charges = List.of(); private List<String> users = List.of(); }
CustomerRequest
public class CustomerRequest { @NotNull(message = "Company id can not be null.") private String company_id; @NotBlank(message = "Name can not be blank.") @Size(max = 255, message = "Name can not be exceed 255 characters.") private String name; @Size(max = 255, message = "Client no. can not be exceed 255 characters.") private String client_no; @Size(max = 15, message = "Phone number can not be exceed 15 digits.") private String phone_number; private FilePart logo; private String logo_path; @NotNull(message = "is_activated cannot be null") @JsonProperty("is_activated") private Boolean is_activated; @NotNull(message = "Created_by cannot be null") private String created_by; @NotNull(message = "Updated_by cannot be null") private String updated_by; }
CustomerController
@PostMapping(value = "/customers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = "application/json") public Mono<ResponseEntity<CustomerResponse>> create(@ModelAttribute @Valid Mono<CustomerRequest> customerRequest) { return customerRequest .flatMap(customerRequestTemp -> customerUseCasePort.create(customerRequestTemp).map(savedCustomerResponse -> ResponseEntity.ok().body(savedCustomerResponse))) .onErrorResume(WebExchangeBindException.class, ex -> Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(customerUseCasePort.getResponseFromWebExchangeBindException(ex)))); }
CustomerControllerTest
@Test void createTest(){ FilePart file = new FilePart() { @Override public String filename() { return "example.jpg"; } @Override public Mono<Void> transferTo(Path dest) { return Mono.empty(); } @Override public String name() { return "example"; } @Override public HttpHeaders headers() { return HttpHeaders.EMPTY; } @Override public Flux<DataBuffer> content() { return DataBufferUtils.read( new ByteArrayResource("name".getBytes(StandardCharsets.UTF_8)), new DefaultDataBufferFactory(), 1024); } }; CustomerRequest customerRequest = new CustomerRequest(); customerRequest.setName("example"); customerRequest.setLogo(file); customerRequest.setLogo_path(""); CustomerResponse customerResponse = new CustomerResponse(); customerResponse.setName("example"); customerResponse.setLogo("example.jpg"); customerResponse.setId("1"); MultipartBodyBuilder builder = new MultipartBodyBuilder(); builder.part("company_id", "1") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"company_id\"") .contentType(MediaType.TEXT_PLAIN); builder.part("name", "example") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"name\"") .contentType(MediaType.TEXT_PLAIN); builder.part("client_no", "1") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"client_no\"") .contentType(MediaType.TEXT_PLAIN); builder.part("phone_number", "123456") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"phone_number\"") .contentType(MediaType.TEXT_PLAIN); builder.part("logo", file); builder.part("logo_path", "") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"logo_path\"") .contentType(MediaType.TEXT_PLAIN); builder.part("is_activated", "1") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"is_activated\"") .contentType(MediaType.TEXT_PLAIN); builder.part("created_by", "1") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"created_by\"") .contentType(MediaType.TEXT_PLAIN); builder.part("updated_by", "1") .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"updated_by\"") .contentType(MediaType.TEXT_PLAIN); Mockito.when(customerUseCasePort.create(customerRequest)).thenReturn(Mono.just(customerResponse)); var test = customerUseCasePort.create(customerRequest).block(); //it will perfect stubbed result. webTestClient .mutateWith(csrf()) .post().uri("/customers") .contentType(MediaType.MULTIPART_FORM_DATA) .accept(MediaType.APPLICATION_JSON) .bodyValue(builder.build()) .exchange() .expectStatus() .isOk(); }
解决提示
- 正确构造FilePart请求参数:使用
MultipartBodyBuilder.part添加文件时,需指定文件名和正确的Content-Disposition头,确保Spring识别为FilePart而非普通字符串。修改logo部分代码:builder.part("logo", file.content(), MediaType.IMAGE_JPEG) .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"logo\"; filename=\"example.jpg\""); - 修正自定义FilePart的name方法:自定义FilePart实现中,
name()方法应返回请求参数名"logo"而非"example",否则Spring无法正确匹配字段。 - 调整控制器参数绑定方式:若自动绑定存在问题,可将文件参数单独接收,再手动组装到CustomerRequest中,例如:
public Mono<ResponseEntity<CustomerResponse>> create( @ModelAttribute @Valid CustomerRequest customerRequest, @RequestPart("logo") Mono<FilePart> logoPart) { return logoPart.flatMap(logo -> { customerRequest.setLogo(logo); return customerUseCasePort.create(customerRequest) .map(saved -> ResponseEntity.ok().body(saved)); }); } - 检查WebFlux配置:确保Spring WebFlux配置中包含Multipart相关的转换器,保证类型转换逻辑正常运行。
内容的提问来源于stack exchange,提问作者arafat
相关产品推荐
相关产品推荐

