1. 概述

在使用 MongoDB 存储大文件时,我们很快就会遇到单文档 16MB 的 BSON 大小限制。这时候,GridFS 就派上用场了 —— 它是 MongoDB 提供的一种将大文件拆分为多个小块(chunks)进行存储的规范。

Spring Data MongoDB 对 GridFS 提供了非常友好的封装,核心是 GridFsOperations 接口及其默认实现 GridFsTemplate。通过它,我们可以像操作普通文件系统一样上传、查询、下载和删除文件,而无需关心底层分块和重组的细节。

✅ 适用场景:存储图片、视频、日志文件等超过 16MB 的二进制数据
❌ 不推荐:小文件(元数据开销大),或频繁随机读写的场景


2. 配置方式

2.1 XML 配置(已逐渐淘汰,了解即可)

如果你还在维护老项目,可能会看到如下 XML 配置:

<bean id="gridFsTemplate" class="org.springframework.data.mongodb.gridfs.GridFsTemplate">
    <constructor-arg ref="mongoDbFactory" />
    <constructor-arg ref="mongoConverter" />
</bean>

<mongo:db-factory id="mongoDbFactory" dbname="filestore" mongo-client-ref="mongoClient" />

<mongo:mapping-converter id="mongoConverter" base-package="com.example.domain">
    <mongo:custom-converters base-package="com.example.converter"/>
</mongo:mapping-converter>

⚠️ 注意:mongoDbFactory 负责连接 MongoDB 实例,mongoConverter 负责 Java 对象与 MongoDB 文档之间的转换。


2.2 Java 配置(推荐)

现代 Spring Boot 项目普遍采用纯 Java 配置,简洁且类型安全:

@Configuration
@EnableMongoRepositories(basePackages = "com.example.repository")
public class MongoConfig extends AbstractMongoClientConfiguration {

    @Autowired
    private MappingMongoConverter mongoConverter;

    @Bean
    public GridFsTemplate gridFsTemplate() throws Exception {
        return new GridFsTemplate(mongoDbFactory(), mongoConverter);
    }

    @Override
    protected String getDatabaseName() {
        return "filestore";
    }

    @Bean
    public MongoClient mongoClient() {
        return MongoClients.create("mongodb://localhost:27017");
    }
}

✅ 小贴士:继承 AbstractMongoClientConfiguration 可自动获得 mongoDbFactory() 方法,减少样板代码。


3. GridFsTemplate 核心方法实战

3.1 store:上传文件

最常用的接口,用于将文件存入 GridFS:

InputStream inputStream = new FileInputStream("src/main/resources/avatar.png");

// 可选:添加自定义元数据
DBObject metaData = new BasicDBObject();
metaData.put("user", "zhangsan");
metaData.put("uploadTime", System.currentTimeMillis());

// 执行存储
ObjectId fileId = (ObjectId) gridFsTemplate.store(
    inputStream, 
    "avatar.png", 
    "image/png", 
    metaData
);

📌 存储原理:

  • 文件元数据(文件名、类型、长度、自定义 metadata 等)存入 fs.files 集合
  • 文件内容被切分为 256KB 的块(默认),存入 fs.chunks 集合
  • 两个集合通过 files_id 字段关联

执行 db['fs.files'].find() 查看元数据:

{
    "_id" : ObjectId("5602de6e5d8bba0d6f2e45e4"),
    "metadata" : {
        "user" : "zhangsan",
        "uploadTime" : 1712345678901
    },
    "filename" : "avatar.png",
    "chunkSize" : NumberLong(261120),
    "uploadDate" : ISODate("2024-04-05T10:20:30.000Z"),
    "length" : NumberLong(855),
    "contentType" : "image/png"
}

执行 db['fs.chunks'].find() 查看内容块(示例只展示第一个 chunk):

{
    "_id" : ObjectId("chunk-001"),
    "files_id" : ObjectId("5602de6e5d8bba0d6f2e45e4"),
    "n" : 0,
    "data" : { "$binary": "iVBORw0KGgoAAAANSUhEUg...", "$type": "00" }
}

⚠️ 踩坑提醒:store() 方法返回的是 ObjectId,不是字符串!强制转 toString() 虽然能用,但建议保留原始类型用于后续操作。


3.2 findOne:精确查询单个文件

根据条件查询最多一个文件,常用于通过 ID 获取文件:

String id = "5602de6e5d8bba0d6f2e45e4";
GridFSFile file = gridFsTemplate.findOne(
    new Query(Criteria.where("_id").is(new ObjectId(id)))
);

📌 行为说明:

  • 如果匹配多个文件,只返回自然排序下的第一条
  • 如果无匹配,返回 null
  • 建议优先使用 _id 查询,性能最优

3.3 find:批量查询文件列表

用于获取满足条件的所有文件元数据:

// 查询所有文件
List<GridFSFile> allFiles = new ArrayList<>();
gridFsTemplate.find(new Query()).into(allFiles);

带条件查询示例:查找 metadata 中 user 为 "zhangsan" 的文件

List<GridFSFile> files = new ArrayList<>();
gridFsTemplate.find(
    new Query(Criteria.where("metadata.user").is("zhangsan"))
).into(files);

✅ 灵活用法:支持 metadata.xxx 路径查询,可结合正则、范围等复杂条件。


3.4 delete:删除文件

删除操作会同时清除 fs.filesfs.chunks 中的记录:

String id = "5702deyu6d8bba0d6f2e45e4";
gridFsTemplate.delete(
    new Query(Criteria.where("_id").is(new ObjectId(id)))
);

⚠️ 踩坑提醒:删除后无法恢复,建议在业务层做软删除标记(如加 deleted: true 到 metadata)。

删除后,数据库中对应文件的元数据和所有 chunk 都会被移除。


3.5 getResources:按文件名模式获取资源

这是一个非常实用的方法,用于通过通配符匹配文件名,返回 GridFsResource 数组,便于后续流式处理:

// 匹配 test 开头的所有文件
GridFsResource[] resources = gridFsTemplate.getResources("test*");

for (GridFsResource resource : resources) {
    String filename = resource.getFilename();
    InputStream inputStream = resource.getInputStream();
    // 处理文件流...
}

📌 示例数据:

[
  { "_id": ObjectId("..."), "filename": "test.png", "metadata": { "user": "alex" } },
  { "_id": ObjectId("..."), "filename": "test_backup.png", "metadata": { "user": "david" } },
  { "_id": ObjectId("..."), "filename": "baeldung.png", "metadata": { "user": "eugen" } }
]

调用 getResources("test*") 将返回前两个文件的资源对象。

✅ 优势:返回的是 Spring 的 Resource 接口,天然集成到 Spring 的资源体系,适合搭配 ResourceHttpRequestHandler 做文件下载接口。


4. GridFSFile 核心方法

GridFSFile 是文件元数据的 Java 表示,常用方法一览:

方法 说明
getFilename() 获取文件名
getMetadata() 获取自定义元数据(DBObject)
getId() 获取文件 ObjectId
getUploadDate() 获取上传时间
getLength() 获取文件总大小(字节)
getContentType() 获取 MIME 类型
containsField(String) 判断是否包含某字段
get(String) 通用字段获取(返回 Object)
keySet() 获取所有字段名

使用示例:

GridFSFile file = gridFsTemplate.findOne(query);
if (file != null) {
    System.out.println("文件名: " + file.getFilename());
    System.out.println("大小: " + file.getLength() + " bytes");
    DBObject meta = file.getMetadata();
    if (meta != null) {
        System.out.println("上传用户: " + meta.get("user"));
    }
}

5. 总结

GridFS 是处理大文件存储的可靠方案,而 GridFsTemplate 极大地简化了开发工作。核心要点回顾:

store:上传文件 + 元数据,自动分块
findOne / find:精准或批量查询元数据
delete:彻底删除文件及分块
getResources:按名匹配,返回 Spring Resource,便于集成下载接口

📌 实战建议:

  • 文件名设计要有唯一性或命名规范,避免冲突
  • 元数据别浪费,记录上传者、业务类型等信息便于检索
  • 大文件下载注意流式处理,避免 OOM
  • 生产环境务必加索引(如 filename, metadata.user

完整示例代码已整理至 GitHub:https://github.com/yourname/spring-mongo-gridfs-demo(mock 地址)


原始标题:GridFS in Spring Data MongoDB

« 上一篇: Java周报 43