使用Axios将pdf文件从Vue前端上传/下载到Spring后端。

3

我使用spring-boot 2.0.4作为我的后端,使用vue 2.5.16 / axios 0.18.0作为我的前端,我想上传PDF文件到我的后端数据库并从前端检索它们。

最初,我受到了这个示例的启发,用于spring部分:https://grokonez.com/frontend/angular/angular-6/angular-6-client-upload-files-download-files-to-mysql-with-springboot-restapis

以及这个gist用于axios部分:https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743

我的代码如下:

  • Uploading file in Vue with axios (the this.file variable is correctly set to my file using an "input type="file" " form input, AxiosService() is simply used to set the correct baseUrl and include an Authorization header with a token):

    createRecord() {
      let formData = new FormData();
      formData.append("file", this.file);
      AxiosService()
        .post("/commands/material/", formData, {
           headers: {
             "Content-Type": "multipart/form-data"
           }
      })
      .then(response => {
        console.log("File uploaded");
      })
      .catch(error => {
        console.log(error);
      });
    
  • The spring part handling the upload looks as follow. In my Entity, the content field is defined as a byte[] annotated with @Lob.

    @BasePathAwareController
    @RequestMapping("/commands/material")
    public class MaterialCommandHandler {
        @Autowired
        MaterialRepository MaterialRepository;
    
        @RequestMapping(method=POST, path = "/")
        public ResponseEntity create(@RequestParam("file") MultipartFile file){
            MaterialEntity material = new MaterialEntity();
            material.setName(file.getOriginalFilename());
            material.setMimetype(file.getContentType());
    
            try {
                material.setContent(file.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            try {
                MaterialRepository.save(material);
            } catch (Exception e) {
                if (e instanceof DataIntegrityViolationException) {
                    throw new InvalidCommandException("Data is invalid for creation.");
                }
                throw(e);
            }
            return ResponseEntity.status(HttpStatus.CREATED).body(material.getId());
    }
    

使用这段代码,条目将正确地创建在数据库中,mysql中的内容字段是longblob类型。

  • The method defined to return the content of the file:

    @RequestMapping(method = GET, path = "/download/{fileId}")
    public ResponseEntity<byte[]> getFile(@PathVariable Long fileId) {
        Optional<MaterialEntity> fileOptional = materialRepository.findById(fileId);
    
        if(fileOptional.isPresent()){
            FrancaisMaterialEntity file = fileOptional.get();
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachement; filename=\"" + file.getName() + "\"");
            return ResponseEntity.ok()
                .headers(headers)
                .body(file.getContent());
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
    }
    
  • Finally the GET method sent from the front-end using axios:

    downloadFile() {
        AxiosService()
          .get(`/commands/material/download/${this.material.id}`, {
            responseType: "blob",
          })
          .then(response => {
            console.log(response);
            const url = window.URL.createObjectURL(new Blob([response.data]));
            const link = document.createElement("a");
            link.href = url;
            link.setAttribute("download", "CalculRanking.pdf");
            document.body.appendChild(link);
            link.click();
            link.parentNode.removeChild(link);
          })
          .catch(error => {
            console.log(error);
            this.errorMessage = error.response.data.message;
          });
      }
    
尝试下载文件时,导航器中的弹出窗口正确显示,但不幸的是下载的.pdf文件似乎已损坏,Chrome指出:“错误:无法加载PDF文档”,我也无法在预览中打开它。我认为问题可能来自于某个过程中内容的错误解释。我进行了大量研究,但我尝试的解决方案都没有使其工作(我尝试的一些事情包括:添加值为“application/pdf”的“Accept”标头,以及在get请求中设置“responseType: arrayBuffer”),因此我决定在这里提问。感谢您的帮助。

如果您使用Postman或任何REST客户端,可以通过调用REST /download/{fileId}来下载PDF文件。 - Abder KRIMA
当我使用Postman调用URL时,我收到一个200 Ok响应,其中Content-Disposition头正确设置为“attachement; filename =”xxx.pdf“”,并且正文包含我的文件的二进制内容(一长串字符)。但是文件没有被下载。 - G.Serneels
我遇到了同样的错误。你能分享一下你的解决方案吗?@G.Serneels - Tuhalang
2个回答

1

你能否通过以下方式更改你的方法getFile

@GetMapping("/download/{fileId}")
@CrossOrigin
@ResponseBody
public ResponseEntity<InputStreamResource> getFile(@PathVariable(required = true, value = "fileId") Long fileId,
        HttpServletRequest request) throws IOException {

    Optional<MaterialEntity> fileOptional = materialRepository.findById(fileId);
    if (ensemblesRegles.isPresent()) {
        String fileName = "example.xlsx";
        MediaType mediaType = MediaType.parseMediaType("application/vnd.ms-excel");
        File file = new File(fileName); //the fileUtils is org.apache.commons.io.FileUtils;
        FileUtils.writeByteArrayToFile(file, fileOptional.get()); // Hope that your get return a byte[] array 
        InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

        return ResponseEntity.ok()
                // Content-Disposition
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + file.getName())
                // Content-Type
                .contentType(mediaType)
                // Contet-Length
                .contentLength(file.length()) //
                .body(resource);
    }
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}

感谢您的回答,经过这些修改后,我现在在我的后端面临一个"org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation"异常。(我在请求中设置了Accept:application/pdf头部,但仍然得到相同的结果) - G.Serneels

1

为什么不看一下Spring Content。它的设计目的正是你想要做的事情,并将一个或多个内容对象与Spring数据实体相关联。

要将其添加到现有的Spring Boot项目中,请执行以下操作:

pom.xml

   <!-- Java API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-jpa-boot-starter</artifactId>
      <version>0.4.0</version>
   </dependency>

   <!-- REST API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-rest-boot-starter</artifactId>
      <version>0.4.0</version>
   </dependency>

MaterialEntity.java

@Entity
public class MaterialEntity {
   @Id
   @GeneratedValue
   private long id;

   ...other existing fields...

   @ContentId
   private String contentId;

   @ContentLength
   private long contentLength = 0L;

   @MimeType
   private String mimeType = "text/plain";

   ...
}

MaterialEntityContentStore.java

@StoreRestResource(path="materialEntityContents")
public interface MaterialEntityContentStore extends ContentStore<MaterialEntity, String> {
}

这是获取REST终端点的全部步骤,它们将允许您存储和检索与每个MaterialEntity相关联的内容。其实现方式非常类似于Spring Data。当您的应用程序启动时,Spring Content将查看'spring-content-jpa-boot-starter'依赖项,并知道您想要在数据库中存储内容。然后,它将创建一个模式来执行此操作,并注入'MaterialEntityContentStore'接口的JPA实现。它还会看到'spring-content-rest-boot-starter',并注入与内容存储接口交互的REST终端点。这意味着您不需要自己处理任何内容。
例如:
curl -X POST /materialEntityContents/{materialEntityId} -F "file=@/path/to/image.jpg"
将图像存储在数据库中,并将其与id为'materialEntityId'的材料实体关联起来。
curl /materialEntityContents/{materialEntity}
将再次获取它等等...实际上还支持完整的CRUD和视频流。

具体来说,这里有一个(非SpringBoot)MySQL示例在这里

您还可以决定将内容存储在其他地方,例如文件系统或S3,只需将spring-content-jpa-boot-starter依赖项替换为适当的Spring Content Storage模块即可。各种存储类型的示例在这里

很遗憾,前端方面没有vuejs示例,但我们确实有一个angularjs 1.x示例在这里。这可能有助于前端,因为它们是类似的技术(根据我有限的经验!)。


谢谢您的回答,我会看一下Spring Content。 - G.Serneels

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接