【问题标题】:How to get a binary stream by GridFS ObjectId with Spring Data MongoDB如何使用 Spring Data MongoDB 通过 GridFS ObjectId 获取二进制流
【发布时间】:2018-08-15 16:09:09
【问题描述】:

当我已经拥有正确的 ObjectId 时,我无法弄清楚如何使用 spring-data-mongodb 及其 GridFSTemplate 从 GridFS 流式传输二进制文件。

GridFSTemplate 返回GridFSResource (getResource()) 或GridFSFile (findX())。

我可以通过 ID 获得GridFSFile:

// no way to get the InputStream?
GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)))

但没有明显的方法可以为该GridFSFile 获取InputStream。

只有GridFSResource 允许我获取对应的InputStream 和InputStreamResource#getInputstream。但获得GridFSResource 的唯一方法是通过filename。

// no way to get GridFSResource by ID?
GridFSResource resource = gridFsTemplate.getResource("test.jpeg");
return resource.getInputStream();

不知何故,GridFsTemplate API 暗示文件名是唯一的——它们不是。 GridFsTemplate 实现只返回第一个元素。

现在我正在使用本机 MongoDB API,一切都变得有意义了:

GridFS gridFs = new GridFs(mongo);
GridFSDBFile nativeFile = gridFs.find(blobId);
return nativeFile.getInputStream();

看起来我误解了 Spring Data Mongo GridFS 抽象背后的基本概念。我希望(至少)以下事情之一是可能的/真实的:

  • 通过其 ID 获取 GridFSResource
  • 获取GridFSResource 或InputStream 以获得GridFsFile 我已经拥有了

我错了还是 Spring Data MongoDB API 的这个特定部分有什么奇怪的地方?

【问题讨论】:

    标签: mongodb spring-data spring-data-mongodb


    【解决方案1】:

    我也偶然发现了这一点。事实上,GridFsTemplate 的设计是这样的,这让我感到非常震惊...... 无论如何,到目前为止,我对此的丑陋“解决方案”:

    public GridFsResource download(String fileId) {
        GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
    
        return new GridFsResource(file, getGridFs().openDownloadStream(file.getObjectId()));
    }
    
    private GridFSBucket getGridFs() {
    
        MongoDatabase db = mongoDbFactory.getDb();
        return GridFSBuckets.create(db);
    }
    

    注意:您必须注入 MongoDbFactory 才能使其工作...

    【讨论】:

    • 所有互联网海上唯一真正的解决方案,非常非常非常非常非常非常非常非常感谢您
    • 是的,我做了完全一样的......只是从spring项目中复制getGridFs函数并实现一个新的getResource。他们应该将其添加到原始代码中。
    • 您如何使用此解决方案处理 mediaType?谢谢
    • 很高兴我不是唯一一个认为 GridFS 的 Mongo 功能和 Spring Data 围绕它的抽象之间存在令人惊讶的阻抗不匹配的人。
    【解决方案2】:

    这些类型有点乱:

    来自 Spring GridFsTemplate source:

    public getResource(String location) {
    
        GridFSFile file = findOne(query(whereFilename().is(location)));
        return file != null ? new GridFsResource(file, getGridFs().openDownloadStream(location)) : null;
    }
    

    有一个丑陋的解决方案:

    @Autowired
    private GridFsTemplate template;
    
    @Autowired
    private GridFsOperations operations;
    
    public InputStream loadResource(ObjectId id) throws IOException {
        GridFSFile file = template.findOne(query(where("_id").is(id)));
        GridFsResource resource = template.getResource(file.getFilename());
    
        GridFSFile file = operations.findOne(query(where("_id").is(id)));
        GridFsResource resource = operations.getResource(file.getFilename());
        return resource.getInputStream();
    }
    

    【讨论】:

    • 我想您终于可以按名称检索文件了。如果您有 2 个同名文件,并且需要第 2 个,则 GridFSFile 文件是正确的 obj,但 GridFsResource 资源是按名称查找的。是哪个?
    【解决方案3】:

    我发现了解决这个问题的方法!

    只需将 GridFSFile 包装在 GridFsResource 中!这是为使用 GridFSFile 实例化而设计的。

    public GridFsResource getUploadedFileResource(String id) {
        var file = this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
        return new GridFsResource(file);
    }
    
    @GetMapping("/{userId}/files/{id}")
    public ResponseEntity<InputStreamResource> getUploadedFile(
        @PathVariable Long userId,
        @PathVariable String id
    ){
        var user = userService
            .getCurrentUser()
            .orElseThrow(EntityNotFoundException::new);
    
        var resource = userService.getUploadedFileResource(id);
    
        try {
            return ResponseEntity
                .ok()
                .contentType(MediaType.parseMediaType(resource.getContentType()))
                .contentLength(resource.contentLength())
                .body(resource);
        } catch (IOException e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    
    
    }
    

    这样做的最大优点是,您可以直接将 GridFsResource 传递给 ResponseEntity,因为 GridFsResource 扩展了 InputStreamResource。

    希望这会有所帮助!

    问候 尼克拉斯

    【讨论】:

      【解决方案4】:

      您是否考虑过使用 Spring Content for Mongo 作为您解决方案中的内容存储部分?

      假设您使用的是 Spring Boot 以及 Spring Data Mongo,那么它可能如下所示:

      pom.xml

      <dependency>
          <groupId>com.github.paulcwarren</groupId>
          <artifactId>spring-content-mongo-boot-starter</artifactId>
          <version>0.0.10</version>
      </dependency>
      <dependency>
          <groupId>com.github.paulcwarren</groupId>
          <artifactId>spring-content-rest-boot-starter</artifactId>
          <version>0.0.10</version>
      </dependency>
      

      使用以下属性更新您的 Spring Data Mongo 实体:

      @ContentId
      private String contentId;
      
      @ContentLength 
      private long contentLength = 0L;
      
      @MimeType
      private String mimeType;
      

      添加商店界面:

      @StoreRestResource(path="content")
      public interface MongoContentStore extends ContentStore<YourEntity, String> {
      }
      

      这就是你所需要的。当您的应用程序启动时,Spring Content 将看到 Spring Content Mongo/REST 模块的依赖关系,它将为 GridFs 注入 MongonContenStore 存储的实现以及支持完整 CRUD 功能并将这些操作映射下来的控制器的实现到底层商店界面。 REST 端点将在/content 下可用。

      即

      curl -X PUT /content/{entityId} 将创建或更新实体的图像

      curl -X GET /content/{entityId} 将获取实体的图像

      curl -X DELETE /content/{entityId} 将删除实体的图像

      有一些入门指南here。他们将 Spring Content 用于文件系统,但模块是可互换的。 Mongo 参考指南是here。还有教程视频here。

      HTH

      【讨论】:

      • 以上文章看起来不错。感谢分享。假设我正在使用上面的 api 上传 pdf、word、文本文件等,现在我想根据用户输入搜索文件内容。如果用户输入了一个文本,它存在于 3 个文件中,所以我想显示 3 个文件。对此有任何建议。
      • 是的,Spring Content 有两个全文索引模块。一个用于 solr,一个用于 elasticsearch。如果您将这些包含在您的类路径中并配置到相关服务器的连接 bean,那么当您添加内容时,它将被发送以进行全文索引,然后可以进行搜索。
      • 这里有一个入门指南:paulcwarren.github.io/spring-content/…
      • Paul,我实施了您提到的解决方案,但是在搜索内容时出现此错误 ->org.elasticsearch.ElasticsearchStatusException: Elasticsearch exception [type=index_not_found_exception, reason=no such index [spring-content-fulltext- index]] 由于需要一些修改,我复制了 DefaultMongoStoreImpl 和 SearchableImpl 的代码,Elasticsearch 也在运行。请建议。
      • @Anand 这个错误通常意味着你运行的是不兼容的 elasticsearch 版本。 Spring Content 1.0.x 针对 6.8.7 进行了测试,Spring Content 1.1.x 目前针对 7.8.2 进行了测试,如果这有帮助吗?
      【解决方案5】:

      将 GridFSFile 包装在 GridFsResource 中或使用它

      GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
      GridFsResource resource = gridFsTemplate.getResource(file);
      return resource.getInputStream();
      

      【讨论】:

        【解决方案6】:

        GridFsTemplate 的getResource(com.mongodb.client.gridfs.model.GridFSFile file) 函数返回GridFSFile 的GridFsResource。

        GridFSFile gridfsFile= gridFsTemplate.findOne(new 
        Query(Criteria.where("filename").is(fileName)));
        GridFsResource gridFSResource= gridFsTemplate.getResource(gridfsFile);
        InputStream inputStream= gridFSResource.getInputStream();
        

        如果上述方法在某些更高版本的 Spring boot 中不起作用,请使用以下方法:

        GridFSFile gridfsFile= gridFsTemplate.findOne(new 
        Query(Criteria.where("filename").is(fileName)));
        //or
        GridFSFile  gridfsFile = 
        gridFsOperations.findOne(Query.query(Criteria.where("filename").is(fileName)));
         return ResponseEntity.ok()
                        .contentLength(gridFsdbFile.getLength())
                        .contentType(MediaType.valueOf("image/png"))
                        .body(gridFsOperations.getResource(gridFsdbFile));
        

        【讨论】:

          【解决方案7】:
          @RequestMapping(value = "/api ")
          public class AttachmentController {
          
          private final GridFsOperations gridFsOperations;
          
          @Autowired
          public AttachmentController(GridFsOperations gridFsOperations) {
              this.gridFsOperations = gridFsOperations;
          }
          
          @GetMapping("/file/{fileId}")
          public ResponseEntity<Resource> getFile(@PathVariable String fileId) {
          GridFSFile file = 
          gridFsOperations.findOne(Query.query(Criteria.where("_id").is(fileId)));
          
              return ResponseEntity.ok()
                      .contentLength(file.getLength())
                      .body(gridFsOperations.getResource(file));
          }
          

          【讨论】:

            【解决方案8】:

            我知道的老问题,但尝试在 2019 年使用 WebFlux 执行此操作,我必须执行以下操作

              public Mono<GridFsResource> getImageFromDatabase(final String id) {
            
                return Mono.fromCallable(
                    () ->
                        this.gridFsTemplate.getResource(
                            Objects.requireNonNull(
                                    this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id))))
                                .getFilename()));
              }
            

            这会给你一个Mono,它可以在控制器中返回。不过,我确信有更好的解决方案。

            【讨论】:

              【解决方案9】:

              Spring Data 2.1.0 向GridFsTemplate 添加了getResource() 的重载,它返回给定GridFsFile 的GridFsResource。 GridFsResource 有一个获取InputStream 的方法。因此,如果您至少使用此版本的 Spring Data,则可以通过两次调用 GridFsTemplate 来获取 InputStream:

              GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));
              
              // In real code, make sure you perform any necessary null checks if the file doesn't exist
              
              GridFsResource resource = gridFsTemplate.getResource(gridFsFile);
              InputStream inputStream = resource.getInputStream();
              

              【讨论】:

                【解决方案10】:
                GridFSDBFile file = ... 
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                file.writeTo(baos);
                byte[] ba = baos.toByteArray()
                

                【讨论】:

                  猜你喜欢
                  • 2013-10-03
                  • 2014-10-19
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2020-06-25
                  • 1970-01-01
                  • 2021-07-06
                  相关资源
                  最近更新 更多