SpringBoot + MongoDB GridFS

随着web 3.0的兴起,数据的形式不局限于文字,还有语音、视频、图片等。高效存储与检索二进制数据也成为web 3.0必须要考虑的问题。然而这种二进制数据是不适合存储在普通关系型数据库(MySQL、Oracle)中的,关系型数据库更多的是存储图片的访问路径。因此二进制数据可以使用MongoDB的内置模块GridFS进行检索与存储,也是一种比较好的解决方案。

1、MongoDB GridFS

1.1、GridFS概述

Grid中文释义为网格,FS(File System)释义为文件系统,合起来就是网格式的文件存储规范或系统。GridFSMongoDB的一个子模块,用于存储和检索超过16M(BSON)的文件,如果文件大小没有超过16M可以将数据保存常规的BSON中。

在实际系统开发中,上传的图片或者文件可能尺寸会很大,此时我们可以借用 GridFS 来辅助管理这些文件。注意:GridFS不是MongoDB自身特性,只是一种将大型文件存储在 MongoDB 的文件规范。这种规范也不是将一个大文件存储在一条文档中,而是自动将文件分成块,每一块作为一条文档单独存储(GridFS使用的块容量默认是256k)。

1.2、GridFS存储原理

GridFS使用两个集合(collection)存储文件:

  1. chunks:用于存储文件内容的二进制数据
  2. files:用于存储文件的元数据(meta数据包括filename、content_type、用户自定义属性等)

GridFS会将两个集合放在一个普通的buket中,并且这两个集合使用buket的名字作为前缀。MongoDBGridFS默认使用fs命名的buket存放两个文件集合。因此存储文件的两个集合分别会命名为集合fs.files和集合fs.chunks

当然也可以自定义不同的buket名字,可以在一个数据库中定义多个bukets,但所有的集合的名字都不得超过MongoDB命名空间的限制(MongoDB 集合的命名包括了数据库名字与集合名字,会将数据库名与集合名通过"."分隔。而且命名的最大长度不得超过120bytes)。

使用GridFS存储文件时,如果文件大于 256K ,会先将文件分割成多个块,最终将 chunk 块的信息存储在fs.chunks集合的多个文档中。然后将文件信息存储在fs.files集合的唯一一份文档中。对于同一个大文件,fs.chunks集合中多个文档中的file_id字段对应fs.files集中某一个文档的_id字段。

除此之外files集合中的文档就是BSON格式存储的,可以使用MongoDB的索引的一系列特性,因此可以更快的遍历与操作文件。

1.3、GridFS应用场景

1、应用系统有需要上传图片(用户上传或者系统本身的文件发布等)

2、文件分布式存储与读取(与其他分布式文件存储系统没啥区别)

3、文件的量级处于飞速增长,有可能达到操作系统自己的文件系统的查询性能瓶颈(甚至超过系统硬盘的扩容范围)

4、文件的备份(不使用GridFS这种方式也可以做,但是更加方面)

5、文件系统访问的故障转移和修复(GridFS可以避免用于存储用户上传内容的文件系统出现的某些问题)

6、文件检索支持索引检索,存储除文件本身以外还需要关联更多的元数据信息(文件的发布式作者、发布时间、文件tag属性等等自定义信息),有了索引后可以更快的检索出文件元数据和文件本身。

7、对文件的分类模糊(文件夹分类关系混乱或者无法分类)

8、文件尺寸较小,而且众多,且文件经常有可能被迁移、删除、修改元数据等

2、文件上传、下载、预览功能实现

2.1、在SpringBoot中集成MongoDB

具体集成细节就不重复说了,请参照这篇文章:

文章地址:https://blog.csdn.net/m0_46357847/article/details/123803461

2.2、JPA与MySQL集成

1、为了方便展示文件属性,我们可以设计一个专门存放图片元数据的数据表(TB_FILE),下载和预览的时候就只需要传入主键即可。例如(JPA + MySQL):

/*** @description: 文件实体* @author: laizhenghua* @date: 2022/6/5 15:28*/
@Entity
@Table(name = "TB_FILE", schema = "TEST")
@JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler"})
public class FileEntity implements Serializable {private static final long serialVersionUID = -6850526733071376712L;private Integer id;private Date addDate;private String guid;private String fullPath;private String fileName;private String size;private String extension;private String state;private Integer sortNo;@Id@Column(name = "ID", unique = true, length = 32)@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}@Column(name = "ADDDATE")public Date getAddDate() {return addDate;}public void setAddDate(Date addDate) {this.addDate = addDate;}@Column(name = "GUID")public String getGuid() {return guid;}public void setGuid(String guid) {this.guid = guid;}@Column(name = "FULLPATH")public String getFullPath() {return fullPath;}public void setFullPath(String fullPath) {this.fullPath = fullPath;}@Column(name = "FILENAME")public String getFileName() {return fileName;}public void setFileName(String fileName) {this.fileName = fileName;}@Column(name = "SIZE")public String getSize() {return size;}public void setSize(String size) {this.size = size;}@Column(name = "EXTENSION")public String getExtension() {return extension;}public void setExtension(String extension) {this.extension = extension;}@Column(name = "STATE")public String getState() {return state;}public void setState(String state) {this.state = state;}@Column(name = "SORTNO")public Integer getSortNo() {return sortNo;}public void setSortNo(Integer sortNo) {this.sortNo = sortNo;}
}

建表SQL:

CREATE TABLE `TEST`.`TB_FILE`  (`ID` int(32) NOT NULL AUTO_INCREMENT,`ADDDATE` datetime NULL DEFAULT NULL,`GUID` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`FULLPATH` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`FILENAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`SIZE` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`EXTENSION` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`STATE` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL,`SORTNO` int(30) NULL DEFAULT NULL,PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

2、Dao层编写

/*** @description: FileDao* @author: laizhenghua* @date: 2022/6/5 15:44*/
public interface FileDao extends JpaRepository<FileEntity, Integer> {// 引入相关GAV坐标后,我们只需继承JAP提供的 JpaRepository 接口就能完成大部分增删改查功能
}

3、编写获取文件实体信息的接口,测试JAP与MySQL集成是否正常。

controller

/*** @description:* @author: laizhenghua* @date: 2022/6/5 15:40*/
@RestController
@RequestMapping(value = "/file")
public class FileController {@Autowiredprivate FileService fileService;@RequestMapping(value = "/getList", method = {RequestMethod.GET, RequestMethod.POST})public R getFileList(@RequestParam(value = "pageNo") Integer pageNo, @RequestParam(value = "pageSize") Integer pageSize) {Map<String, Object> fileMap = fileService.getList(pageNo, pageSize);return R.ok().put("data", fileMap);}
}

service

@Service(value = "fileService")
public class FileServiceImpl implements FileService {private final org.apache.logging.log4j.Logger log = Logger.getLogger(FileServiceImpl.class);@Autowiredprivate FileDao fileDao;@Autowiredprivate GridFsOperations gridFsOperations;@Autowiredprivate GridFsTemplate gridFsTemplate;@Overridepublic Map<String, Object> getList(Integer pageNo, Integer pageSize) {Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.Direction.ASC, "id");Page<FileEntity> pageResult = fileDao.findAll(pageable);if (pageResult == null) {return null;}List<FileEntity> fileEntityList = pageResult.getContent();if (CollectionUtils.isEmpty(fileEntityList)) {return null;}Map<String, Object> resultMap = new HashMap<>();resultMap.put("total", pageResult.getTotalElements());resultMap.put("list", fileEntityList);return resultMap;}
}

4、随便插入几条数据进行测试,例如

2.3、文件上传实现

设计好实体后,我们采用Spring封装好的GridFsOperations进行文件二进制数据的保存也就很简单了,例如:

controller

@RestController
@RequestMapping(value = "/file")
public class FileController {@Autowiredprivate FileService fileService;@RequestMapping(value = "/upload", method = RequestMethod.POST)public R upload(@RequestParam(value = "file") MultipartFile file) {FileEntity fileEntity = fileService.upload(file);if (fileEntity == null) {return R.error(500, "文件上传失败");}return R.ok().put("data", fileEntity);}
}

service层,主要看下uoload方法即可。

/*** @description:* @author: laizhenghua* @date: 2022/6/5 15:42*/
@Service(value = "fileService")
public class FileServiceImpl implements FileService {private final org.apache.logging.log4j.Logger log = Logger.getLogger(FileServiceImpl.class);@Autowiredprivate FileDao fileDao;@Autowiredprivate GridFsOperations gridFsOperations;@Autowiredprivate GridFsTemplate gridFsTemplate;@Overridepublic Map<String, Object> getList(Integer pageNo, Integer pageSize) {Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.Direction.DESC, "id");Page<FileEntity> pageResult = fileDao.findAll(pageable);if (pageResult == null) {return null;}List<FileEntity> fileEntityList = pageResult.getContent();if (CollectionUtils.isEmpty(fileEntityList)) {return null;}Map<String, Object> resultMap = new HashMap<>();resultMap.put("total", pageResult.getTotalElements());resultMap.put("list", fileEntityList);return resultMap;}@Overridepublic FileEntity upload(MultipartFile file) {InputStream inputStream = null;ObjectId _id = null;try {inputStream = file.getInputStream();_id = gridFsOperations.store(inputStream, file.getOriginalFilename(), file.getContentType());} catch (IOException e) {log.error(e);} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {log.error(e);}}}if (_id == null) {return null;}// 保存文件实体对象FileEntity entity = saveFileEntity(_id, file);return entity;}public FileEntity saveFileEntity(ObjectId _id, MultipartFile file) {FileEntity entity = new FileEntity();entity.setGuid(_id.toString());entity.setAddDate(new Date());entity.setExtension(getExtensionName(file.getOriginalFilename()));entity.setFullPath("/file/" + _id + "." + getExtensionName(file.getOriginalFilename()));entity.setFileName(file.getOriginalFilename());entity.setSize(getFileSize(file.getSize(), 2));entity.setSortNo(999);entity.setState("0");return fileDao.save(entity);}// 文件后缀获取public String getExtensionName(String fileName) {if (fileName == null || fileName.length() == 0) {return null;}int index = fileName.lastIndexOf(".");if (index < 0 || index > fileName.length() - 1) {return null;}return fileName.substring(index + 1);}// 获取文件大小KBpublic String getFileSize(long size, int round) {BigDecimal decimal = new BigDecimal(size / 1024);return decimal.setScale(round, BigDecimal.ROUND_HALF_UP).doubleValue() + "KB";}@Overridepublic String getGridFSDatabaseName() {Class<? extends GridFsTemplate> clazz = gridFsTemplate.getClass();MongoDatabaseFactory dbFactory = null;try {Field dbFactoryField = clazz.getDeclaredField("dbFactory");dbFactoryField.setAccessible(true);dbFactory = (MongoDatabaseFactory) dbFactoryField.get(gridFsOperations);} catch (Exception e) {log.error(e);return null;}if (dbFactory == null) {return null;}MongoDatabase mongoDatabase = dbFactory.getMongoDatabase();if (mongoDatabase == null) {return null;}return mongoDatabase.getName();}
}

最后我们可以使用VUEelement-ui简单搭建一个前端页面进行文件上传、下载、预览功能测试。VUE在国内还是比较重要的,推荐大家还是自行搭建下前端页面。例如:


那么如上图所示,[点击上传]按钮如何如何配合后端接口实现文件上传呢?

1、前端可以声明一个隐藏起来的input标签

<input type="file" style="display: none" ref="file" @change="fileUpload($event)"/>
<div class="upload"><el-button size="small" type="primary" @click="upload">点击上传</el-button>
</div>

2、给[点击上传]按钮绑定点击事件(不借助任何上传文件插件)事件回调函数如下

upload() {// 主动触发点击事件this.$refs.file.dispatchEvent(new MouseEvent("click"));
}
// 点击[点击上传]按钮后会弹出文件上传窗口。

3、最后就是选择文件后的回调函数@change写法,也是在这个回调函数中完成文件上传接口的调用

fileUpload(event) {// 通过event.target.files获取文件内容并封装到formData中let file = event.target.files[0];let formData = new FormData();formData.append("file", file);// 文件上传let _ctx = this;request.upload(formData).then(function (resp) {if (resp.code === 500) {_ctx.$message.error(resp.msg);}_ctx.$message({message: '文件上传成功', type: 'success'});_ctx.getFileList(); // 刷新table数据});
}

4、测试与验证文件元数据与二进制数据在MongoDB中保存的位置

选择文件上传

查看表格数据

我们可以在表格中可以看到,文件的GUID=62a4048b97ba1849b54df139也就是MongoDB中主键,那么就可以通过这个主键找到此文件的元数据与二进制数据,例如:

// fs.files 集合保存元数据
db.getCollection("fs.files").find({_id: ObjectId("62a4048b97ba1849b54df139")});// fs.chunks 集合保存二进制数据
db.getCollection("fs.chunks").find({files_id: ObjectId("62a4048b97ba1849b54df139")});


2.4、文件预览实现

对于文件的预览也很简单,在浏览器中预览需要有承载文件的标签页,也需要告诉浏览器文件以什么方式处理文件也就是使用Content-Type控制,例如Content-Type: image/png。老样子我们还是先写后端接口。备注:预览和下载我们可以通过一个接口实现。

controller

@RestController
@RequestMapping(value = "/file")
public class FileController {@Autowiredprivate FileService fileService;@RequestMapping(value = "/download/{id}", method = RequestMethod.GET)public void download(@PathVariable("id") Integer fileId, @RequestParam(value = "type", required = false) String type, HttpServletResponse response) {// preview/预览 download/下载type = type == null ? "download" : type;fileService.download(fileId, type, response);}
}

service

/*** @description:* @author: laizhenghua* @date: 2022/6/5 15:42*/
@Service(value = "fileService")
public class FileServiceImpl implements FileService {private final org.apache.logging.log4j.Logger log = Logger.getLogger(FileServiceImpl.class);@Autowiredprivate FileDao fileDao;@Autowiredprivate GridFsOperations gridFsOperations;@Overridepublic void download(Integer fileId, String type, HttpServletResponse response) {FileEntity entity = fileDao.getById(fileId);if (entity == null) {return;}Query query = Query.query(Criteria.where("_id").is(new ObjectId(entity.getGuid())));GridFSFile gridFSFile = gridFsOperations.findOne(query);if (gridFSFile == null) {return;}GridFsResource resource = gridFsOperations.getResource(gridFSFile);InputStream inputStream = null;OutputStream outputStream = null;try {inputStream = resource.getInputStream();outputStream = response.getOutputStream();response.setContentType(resource.getContentType());if ("download".equals(type)) {String fileName = URLEncoder.encode(resource.getFilename(), "utf-8");response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");}write(inputStream, outputStream);} catch (IOException e) {log.error(e);} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException e) {log.error(e);}}if (outputStream != null) {try {outputStream.close();} catch (IOException e) {log.error(e);}}}}public void write(InputStream inputStream, OutputStream outputStream) {byte[] buffer = new byte[4096];try {int count = inputStream.read(buffer, 0, buffer.length);while (count != -1) {outputStream.write(buffer, 0, count);count = inputStream.read(buffer, 0, buffer.length);}} catch (RuntimeException e) {throw e;} catch (Exception e) {throw new RuntimeException(e.getMessage(), e);}}
}

OK后端接口搞定,接下来就是前端按钮功。前面我们也说要想实现文件预览需要有两个东西一个标签页另外一个是Content-Type。实际开发中都需要借助第三方插件去承载文件,还可以对文件进行放大与缩小等操作。但是在这里为了方便我们直接以浏览器标签页去承载文件即可。

1、前端按钮代码

<el-table-column label="操作" width="150" align="center"><template slot-scope="scope"><el-button type="primary" size="small" @click="preview(scope.row)">预览</el-button><el-button type="success" size="small" @click="download(scope.row)">下载</el-button></template>
</el-table-column>

2、preview点击事件回调函数

preview(row) {// 重新打打开一个标签页即可window.open("http://127.0.0.1:8080/file/download/" + row.id + "?type=preview");
}

3、测试

点击预览按钮即可

2.5、文件下载实现

前面我们也说了预览和下载是共用一个接口,只需修改参数即可(至于后端下载接口详见预览后端接口)。因此我们只需书写前端代码即可,例如:

download(row) {let url = "http://127.0.0.1:8080/file/download/" + row.id;// 手动创建一个form标签let form = document.createElement("form");form.method = "get";form.action = url; // url即为下载接口document.body.append(form);form.submit();
}

测试

END

SpringBoot+MongoDB GridFS文件上传、下载、预览实战相关推荐

  1. uploadify java 下载_uploadify java实现多文件上传和预览

    本文实例为大家分享了java文件上传和预览实现代码,供大家参考,具体内容如下 1.下载uploadify插件 2.index.html #uploader { position: relative; ...

  2. php 表格导入excel插件,BootStrap Fileinput插件和表格插件相结合实现导入Excel数据的文件上传、预览、提交的步骤...

    这篇文章主要介绍了BootStrap Fileinput插件和Bootstrap table表格插件相结合实现文件上传.预览.提交的导入Excel数据操作步骤,需要的朋友可以参考下 bootstrap ...

  3. php案例 文件上传并预览

    作者:陈业贵 华为云享专家 51cto(专家博主 明日之星 TOP红人) 阿里云专家博主 文章目录 前言 代码 cyg.php 2.php 效果:也就是上传的文件里面的内容 前言 php案例 文件上传 ...

  4. springboot:实现文件上传下载实时进度条功能【附带源码】

    0. 引言 记得刚入行的时候,做了一个文件上传的功能,因为上传时间较久,为了用户友好性,想要添加一个实时进度条,显示进度.奈何当时技术有限,查了许久也没用找到解决方案,最后不了了之. 近来偶然想到这个 ...

  5. 基于Mongodb的文件上传下载,图片预览

    1.依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-b ...

  6. servlet实现文件上传,预览,下载和删除

    一.准备工作 1.1 文件上传插件:uploadify: 1.2 文件上传所需jar包:commons-fileupload-1.3.1.jar和commons-io-2.2.jar 1.3 将数据转 ...

  7. kl-uploads 多文件上传与预览的实现

    功能描述 多文件的上传,图片添加预览功能,非图片只有名称列表 使用 <template><div class="demo"><klUploadhide ...

  8. SRC挖洞之文件上传/下载漏洞的实战案例

    文章目录 前言 任意文件下载 案例1 某OA系统任意文件下载 案例2 某登录页面任意文件下载 案例3 某金融网站任意文件下载 案例4 服务端过滤 ../ 绕过下载 案例5 %00截断后下载任意文件 文 ...

  9. javascript --- 文件上传即时预览 闭包实现多图片即时预览

    使用javascript原生功能实现,点击上传文件,然后再网页上显示出来 1. 初级显示 1.1 准备一个input标签和一个img标签 <input type=file id="fi ...

最新文章

  1. 解析IT行业的苦逼工作!(漫画)
  2. 我的大学到研究生自学 Java 之路,过程艰辛,不放弃,保持热情,最终发现我是这样拿到大厂 offer 的!...
  3. 最大和 -- 最大子矩阵
  4. mysql会对同时读取加锁吗_程序员经典面试题,MySQL并发读写的时候,都是需要加锁的么?...
  5. Python缩小图像
  6. 阿克苏计算机考试成绩查询,阿克苏高考成绩查询系统2021
  7. python爬虫爬取微信公众号的阅读数、喜爱数、文章标题和链接等信息
  8. Rasa3 domain官方文档翻译
  9. 如何用计算机基础知识提问,职业学校《计算机应用基础》课的提问策略
  10. String format格式化
  11. element ui自定义图标
  12. html onload状态事件,HTML onload事件用法及代码示例
  13. 突发收购,亚信科技在谋划什么?
  14. “三高“Mysql - Mysql备份概览
  15. java cause_Cause: java.lang.UnsupportedOperationException
  16. M1芯片的Mac安装Centos !
  17. java读Excel转List对象
  18. 如何维护香港银行账户,避免账户被关闭冻结
  19. 读完 DALL-E 论文,我们发现大型数据集也有平替版
  20. 优秀手机应用设计需要遵循的8大原则

热门文章

  1. web连接mysql教程视频_jsp servlet mysql实现的Java web在线商城项目源码附带视频指导运行教程...
  2. Flutter 2.0 发布 | 针对 Web,移动端和桌面端构建的下一代 Flutter
  3. 农学跨专业考研计算机,跨专业考研依然可以得高分
  4. GlobalSign即将停止签发SHA1代码签名证书
  5. AlphaSSL证书和GlobalSign SSL证书介绍
  6. windows getLastError 错误码大全
  7. Django开源项目
  8. 怎么自定义服务器的404,如何自定义404页面
  9. java中各种加密算法的实践应用
  10. 网络中丢包的原因及类型