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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 02:08:14