如何使用 Spring Data MongoDB 通过 GridFS ObjectId 获取二进制流

Posted

技术标签:

【中文标题】如何使用 Spring Data MongoDB 通过 GridFS ObjectId 获取二进制流【英文标题】:How to get a binary stream by GridFS ObjectId with Spring Data MongoDB 【发布时间】: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 允许我获取对应的InputStreamInputStreamResource#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 获取GridFSResourceInputStream 以获得GridFsFile 我已经拥有了

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

【问题讨论】:

【参考方案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】:

这些类型有点乱:

GridFSFile 是来自 MongoDB 驱动程序的类型 GridFsResource 是 Spring 的类型 ObjectId 是来自 BSON API 的类型

来自 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() 的重载,它返回给定GridFsFileGridFsResourceGridFsResource 有一个获取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()

【讨论】:

以上是关于如何使用 Spring Data MongoDB 通过 GridFS ObjectId 获取二进制流的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB Aggregation - 如何使用 spring-data-mongodb 将查询表达式应用到匹配阶段?

如何利用spring data mongodb 进行多条件查询

如何使用 Spring Data MongoDB 通过 GridFS ObjectId 获取二进制流

Spring Data MongoDB:如何实现“实体关系”?

如何在 Spring Data MongoDB 中使用乐观锁定?

如何在 Spring-data 中更改/定义 Mongodb 的默认数据库?