网站搜索

angular - 是否可以在 httpClient.post 中发送 FormData 对象数组?


我有一个 Angular 前端应用和一个 Java 后端,用户可以选择一个文档,然后保存它。

为此,我一直在无问题地将 FormData 对象从前端发送到后端。

这是我目前在前端所拥有的一段代码,它可以将单个文件的内容发送到后端:

public postSaveDocument(entityType: DocumentEntityType, id: number, body: DocumentInputDto): Observable<Upload<UploadWithLockResultsDto>> {

    const url = `${DocumentationService.baseUrl}/${getUrlSegment(entityType)}/${id}/documents`;

     //Create a FormData object and set the values of the document
     const formData = new FormData();
     formData.append('documentId', this.documentId.toString());
     formData.append('title', this.title);
     formData.append('content', this.content, this.content.name);
    


    return this.httpClient.post(url, formData,
      {...DocumentationService.options, reportProgress: true, observe: 'events', responseType: 'json'})
      .pipe(upload<UploadWithLockResultsDto>());



}

这是我在后台保存该文件的方法:

@PostMapping("/{documentEntityType}/{entityId}/documents")
    public ResponseEntity<UploadWithLockResultsDto> saveDocument(
           @PathVariable("documentEntityType") DocumentEntityType documentEntityType,
           @PathVariable("entityId") Integer entityId,
           DocumentInputDto documentInputDto,
                                   ) throws AuthException, IOException {

           //Save the updates to the database
           var uploadWithLockResultsDto = documentService.documentUpdate(documentInputDto, authentication.getName(), authentication.getPrincipal());
           return handleUploadWithLockResults(uploadWithLockResultsDto);
   
}

这对于保存单个文档来说完全可行。

现在我想做的是允许用户一次选择多个文档,并在将它们发送到后端时一次性保存它们。

到目前为止,这方面没有运气。

我尝试过在前端把数据放入一组 FormData 对象中

public postSaveDocuments(entityType: DocumentEntityType, id: number, body: DocumentInputDto[]): Observable<Upload<UploadWithLockResultsDto>> {

    const formDataArray: any[] = [];
    body.forEach(documentInputDto => {

      const formData = new FormData();
      formData.append('documentId', this.documentId.toString());
      formData.append('title', this.title);
      formData.append('content', this.content, this.content.name);
      formDataArray.push(formData);
    });

    return this.httpClient.post(url, formDataArray,
      {...DocumentationService.options, reportProgress: true, observe: 'events', responseType: 'json'})
      .pipe(upload<UploadWithLockResultsDto>());
}

然后我试着在后端把它作为数组捞起来

@PostMapping("/{documentEntityType}/{entityId}/documents")
    public ResponseEntity<UploadWithLockResultsDto> saveDocument(
           @PathVariable("documentEntityType") DocumentEntityType documentEntityType,
           @PathVariable("entityId") Integer entityId,
           DocumentInputDto[] documentInputDtos,
                                   ) throws AuthException, IOException {

           //Save the docs to the database
           var uploadWithLockResultsDto = documentService.updateDocuments(documentInputDtos, authentication.getName(), authentication.getPrincipal());
           return handleUploadWithLockResults(uploadWithLockResultsDto);
   
}

不足为奇,这行不通。它甚至根本没有到达后端。我意识到,从前端发送一组 FormData 对象到后端可能本就不是其设计用途。

有人能告诉我如何修改这个调用,以便从前端发送多个 FormData 对象到后端吗?

非常感谢。

答案#1

你走在正确的路上,但直接从前端将多个 FormData 对象以数组的形式发送到后端是行不通的,因为 FormData 对象是作为一个单一实体发送的。相反,你应该使用多部分表单请求,将文档作为单独的部分包含在一个 FormData 对象中发送。

步骤 1:前端 - 修改 FormData 对象以处理多个文件 要发送多个文件,您可以将每个文档的数据附加到同一个 FormData 对象中,并为每个文档使用唯一的键。这将允许您在一次 HTTP 请求中发送多个文档。

更新的前端代码:

public postSaveDocuments(entityType: DocumentEntityType, id: number, body: DocumentInputDto[]): Observable<Upload<UploadWithLockResultsDto>> {

    const url = `${DocumentationService.baseUrl}/${getUrlSegment(entityType)}/${id}/documents`;

    // Create a single FormData object for all documents
    const formData = new FormData();

    // Loop through the document DTOs and append each one
    body.forEach((documentInputDto, index) => {
      formData.append(`documents[${index}].documentId`, this.documentId.toString());
      formData.append(`documents[${index}].title`, this.title);
      formData.append(`documents[${index}].content`, this.content, this.content.name);
    });

    return this.httpClient.post(url, formData, {
      ...DocumentationService.options,
      reportProgress: true,
      observe: 'events',
      responseType: 'json'
    }).pipe(upload<UploadWithLockResultsDto>());
}

步骤2:后端 - 处理多部分表单数据 在后端,你需要处理多个文件及其相关的元数据。你可以通过将数据作为MultipartFile数组或自定义DTO来接收,具体取决于你希望如何组织请求。由于你正在发送元数据和文件,你可以结合使用@RequestParam和@ModelAttribute注解来绑定表单数据和文件内容。

更新后的后端代码:

@PostMapping("/{documentEntityType}/{entityId}/documents")
public ResponseEntity<UploadWithLockResultsDto> saveDocuments(
    @PathVariable("documentEntityType") DocumentEntityType documentEntityType,
    @PathVariable("entityId") Integer entityId,
    @RequestParam("documents") List<MultipartFile> documents,
    @RequestParam Map<String, String> documentMetadata) throws AuthException, IOException {

    // Parse metadata and files
    List<DocumentInputDto> documentInputDtos = new ArrayList<>();
    for (int i = 0; i < documents.size(); i++) {
        MultipartFile file = documents.get(i);
        String documentId = documentMetadata.get("documents[" + i + "].documentId");
        String title = documentMetadata.get("documents[" + i + "].title");

        DocumentInputDto documentInputDto = new DocumentInputDto();
        documentInputDto.setDocumentId(Long.parseLong(documentId));
        documentInputDto.setTitle(title);
        documentInputDto.setContent(file); // Assuming the DocumentInputDto has a 'content' field for the file

        documentInputDtos.add(documentInputDto);
    }

    // Save the documents to the database
    var uploadWithLockResultsDto = documentService.updateDocuments(documentInputDtos, authentication.getName(), authentication.getPrincipal());
    return handleUploadWithLockResults(uploadWithLockResultsDto);
}

步骤 3:更新你的 DocumentInputDto

public class DocumentInputDto {
    private Long documentId;
    private String title;
    private MultipartFile content; // The file being uploaded
    
    // Getters and setters...`enter code here`
}

答案#2

你在处理 documents 数组返回为 null 时遇到的问题,很可能是由于 Spring 处理表单数据和多部分文件上传的方式。问题可能在于,你在请求中将文件和表单数据(如元数据)使用相同的名称(documents[])发送,但 Spring 可能没有正确将 MultipartFile 部分映射到 List documents。

让我们来讲解一些你可以进行的调整,以确保 Spring 正确地将文件数据和元数据映射到适当的字段。

第1步:更新前端代码\n您已经在单个 FormData 对象中发送带有元数据的文件。关键问题是确保请求的结构正确,以便后端能够正确识别文件和元数据。文件需要为每个文档分别发送名称字段,并且元数据应遵循相同的命名约定。

这是更新后的前端代码,用于正确追加多个文档:

public postSaveDocuments(entityType: DocumentEntityType, id: number, body: DocumentInputDto[]): Observable<Upload<UploadWithLockResultsDto>> {
    const url = `${DocumentationService.baseUrl}/${getUrlSegment(entityType)}/${id}/documents`;

    // Create a single FormData object to handle multiple files and their metadata
    const formData = new FormData();

    // Loop through the document DTOs and append each one to the formData object
    body.forEach((documentInputDto, index) => {
        // Append metadata fields
        formData.append(`documents[${index}].documentId`, documentInputDto.documentId.toString());
        formData.append(`documents[${index}].title`, documentInputDto.title);

        // Append the file for this document
        formData.append(`documents[${index}].content`, documentInputDto.content, documentInputDto.content.name);
    });

    // Send the FormData as a POST request
    return this.httpClient.post(url, formData, {
        ...DocumentationService.options,
        reportProgress: true,
        observe: 'events',
        responseType: 'json'
    }).pipe(upload<UploadWithLockResultsDto>());
}

步骤 2:更新后端代码\nSpring 使用 @RequestParam 来绑定表单数据和文件上传。解决此问题的关键是确保元数据和文件正确绑定到后端参数。以下是如何调整后端以正确处理文件上传和元数据的方法:

MultipartFile 数组:Spring 的 @RequestParam 会将使用相同名称发送的多个文件映射到一个 List。元数据映射:要映射文档元数据(例如 documentId 和 title),可以使用一个 map 或自定义 DTO。以下是更新后的后端方法:

 @PostMapping("/{documentEntityType}/{entityId}/documents")
    public ResponseEntity<UploadWithLockResultsDto> saveDocuments(
        @PathVariable("documentEntityType") DocumentEntityType documentEntityType,
        @PathVariable("entityId") Integer entityId,
        @RequestParam("documents") List<MultipartFile> documents,  // Receive the files
        @RequestParam Map<String, String> documentMetadata) throws AuthException, IOException {
    
        // Ensure the documents and metadata are mapped correctly
        List<DocumentInputDto> documentInputDtos = new ArrayList<>();
        
        for (int i = 0; i < documents.size(); i++) {
            MultipartFile file = documents.get(i);
    
            // Retrieve metadata for each document
            String documentId = documentMetadata.get("documents[" + i + "].documentId");
            String title = documentMetadata.get("documents[" + i + "].title");
    
            // Create DocumentInputDto from metadata and file
            DocumentInputDto documentInputDto = new DocumentInputDto();
            documentInputDto.setDocumentId(Long.parseLong(documentId));
            documentInputDto.setTitle(title);
            documentInputDto.setContent(file); // Assuming 'content' is a MultipartFile field in your DTO
    
            documentInputDtos.add(documentInputDto);
        }
    
        // Save the documents to the database (service call)
        var uploadWithLockResultsDto = documentService.updateDocuments(documentInputDtos, authentication.getName(), authentication.getPrincipal());
        return handleUploadWithLockResults(uploadWithLockResultsDto);
    }

步骤 3:确保 DocumentInputDto 正确 确保您的 DocumentInputDto 类有一个用于 MultipartFile 的字段,并且您正确使用了 getter 和 setter。以下是 DocumentInputDto 类的一个简单版本:

public class DocumentInputDto {
    private Long documentId;
    private String title;
    private MultipartFile content; // The file being uploaded

    // Getters and setters
    public Long getDocumentId() {
        return documentId;
    }

    public void setDocumentId(Long documentId) {
        this.documentId = documentId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public MultipartFile getContent() {
        return content;
    }

    public void setContent(MultipartFile content) {
        this.content = content;
    }
}

步骤 4:处理多部分配置(如有需要) 确保您的 Spring Boot 应用程序已配置以处理多部分文件上传。通常在 application.properties 或 application.yml 文件中进行配置。以下是配置方法:

application.properties
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB