springboot-GridFS上传下载,小程序正常使用

448 阅读6分钟

1.MongoDB-GridFS介绍

1.1特点

官方文档

  • GridFS 是一种用于存储和检索超过 BSON 文档大小限制 16 MB 的文件的规范。
  • GridFS 默认将每个文件分割为255kB 的块,来进行存储
  • GridFS 用两个集合来存储一个文件,分别是fs.filesfs.chunksfiles用来存储文件元信息,chunks用来存储文件块

2.准备工作

2.1 MongoDB的starter

引入MongoDB所需要的依赖包

pom.xml

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-mongodb</artifactId>
 </dependency>

2.2 MongoDB连接配置

application.yml

 spring:
   data:
     mongodb:
       uri: mongodb://localhost:27017/gridFS-test
   servlet:
     multipart:
       max-file-size: 10MB

3.GridFs文件上传

3.1 封装文件桶

在gridFS中有bucket的概念,整个文件的检索路径为database->bucket,在分布式系统中,如果需要文件共享的bucket所在的database和数据所在database不是同一个,就不能直接使用注入GridFsTemplate来存取文件,所以封装了拿到公共bucket的过程。

 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.gridfs.GridFSBucket;
 import com.mongodb.client.gridfs.GridFSBuckets;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.MongoTemplate;
 ​
 ​
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 /**
  *  文件上传下载
  * @author WangKangSheng
  * @date 2022-06-05 18:07
  */
 @RestController
 @RequestMapping("/file")
 public class FileController {
 ​
     @Autowired
     private MongoTemplate mongoTemplate;
 ​
     /**
      * 获得默认gridFS bucket,
      *  指定上传文件的库为 'def_gfs'
      *   这样做的好处是,多个系统使用不同的数据库连接,可以通过指定统一的数据库来创建GridFSBucket
      * @param mongoTemplate MongoTemplate
      * @return GridFSBucket
      */
     private GridFSBucket defaultGridFsBucket(MongoTemplate mongoTemplate){
         if (null == mongoTemplate){
             throw new NullPointerException("获取默认的GridFSBucket时mongoTemplate不能为空");
         }
         // 通过mongoTemplate来获取默认的数据库连接
         MongoDatabaseFactory mongoDatabaseFactory = mongoTemplate.getMongoDatabaseFactory();
         MongoDatabase db = mongoDatabaseFactory.getMongoDatabase("def_gfs");
 ​
         // 创建GridFSBucket 并指定文件系统使用的bucket
         return GridFSBuckets.create(db,"def_bucket");
     }
 }

3.2文件上传接口

拿到GridFSBucket之后可以通过GridFSBucket#uploadFromStream()方法来上传文件到GridFS.

image-20220605190253172.png

该方法有8个重载方法,这一次使用**ObjectId uploadFromStream(String filename, InputStream source);** 来上传文件。

可以通过指定第三个参数GridFSUploadOptions来添加一些自定义的信息

 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.gridfs.GridFSBucket;
 import com.mongodb.client.gridfs.GridFSBuckets;
 import org.bson.types.ObjectId;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 ​
 import java.io.IOException;
 ​
 /**
  *  文件上传下载
  * @author WangKangSheng
  * @date 2022-06-05 18:07
  */
 @RestController
 @RequestMapping("/file")
 public class FileController {
 ​
     @Autowired
     private MongoTemplate mongoTemplate;
 ​
     /**
      * 获得默认gridFS bucket,
      *  指定上传文件的库为 'def_gfs'
      *   这样做的好处是,多个系统使用不同的数据库连接,可以通过指定统一的数据库来创建GridFSBucket
      * @param mongoTemplate MongoTemplate
      * @return GridFSBucket
      */
     private GridFSBucket defaultGridFsBucket(MongoTemplate mongoTemplate){
         if (null == mongoTemplate){
             throw new NullPointerException("获取默认的GridFSBucket时mongoTemplate不能为空");
         }
         // 通过mongoTemplate来获取默认的数据库连接
         MongoDatabaseFactory mongoDatabaseFactory = mongoTemplate.getMongoDatabaseFactory();
         MongoDatabase db = mongoDatabaseFactory.getMongoDatabase("def_gfs");
 ​
         // 创建GridFSBucket 并指定文件系统使用的bucket
         return GridFSBuckets.create(db,"def_bucket");
     }
 ​
     /**
      * 上传文件到gfs
      * @param file MultipartFile 文件
      * @return String
      * @throws IOException 读取输入流时的异常
      */
     @PostMapping("/upload")
     public String upload(MultipartFile file) throws IOException {
 ​
         // 拿到gfs的bucket
         GridFSBucket gridFsBucket = defaultGridFsBucket(this.mongoTemplate);
 ​
         // 读取文件名
         String filename = file.getOriginalFilename();
 ​
         // 上传文件到gfs  并得到返回的object id
         ObjectId objectId = gridFsBucket.uploadFromStream(filename, file.getInputStream());
 ​
         return "文件上传成功!文件名为:[%s] 文件id为:[%s]".formatted(filename,objectId.toHexString());
     }
     
 }

3.3postman测试上传

3.3.1 上传文件

image-20220605191123476.png

3.3.2 查看gridFS

文件已经上传到我们指定的database和bucket中了

image-20220605191231179.png

文件信息

image-20220605191305057.png

3.3.3查看集合信息

使用命令可以看到**files**集合中的文件信息

image-20220605191433600.png

查看**chunks**集合的内容

集合中存储着文件内容和对应的文件id

image-20220605191733725.png

4.下载文件

4.1 文件下载接口

通过**GridFSBucket#downloadToStream()** 直接将文件写出到输出流

也可以使用**GridFSBucket#openDownloadStream()** 来打开输出流,自己选择处理流

 ​
 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.gridfs.GridFSBucket;
 import com.mongodb.client.gridfs.GridFSBuckets;
 import com.mongodb.client.gridfs.model.GridFSFile;
 import org.bson.Document;
 import org.bson.types.ObjectId;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.URLEncoder;
 ​
 /**
  *  文件上传下载
  * @author WangKangSheng
  * @date 2022-06-05 18:07
  */
 @RestController
 @RequestMapping("/file")
 public class FileController {
 ​
     @Autowired
     private MongoTemplate mongoTemplate;
 ​
     /**
      * 获得默认gridFS bucket,
      *  指定上传文件的库为 'def_gfs'
      *   这样做的好处是,多个系统使用不同的数据库连接,可以通过指定统一的数据库来创建GridFSBucket
      * @param mongoTemplate MongoTemplate
      * @return GridFSBucket
      */
     private GridFSBucket defaultGridFsBucket(MongoTemplate mongoTemplate){
         if (null == mongoTemplate){
             throw new NullPointerException("获取默认的GridFSBucket时mongoTemplate不能为空");
         }
         // 通过mongoTemplate来获取默认的数据库连接
         MongoDatabaseFactory mongoDatabaseFactory = mongoTemplate.getMongoDatabaseFactory();
         MongoDatabase db = mongoDatabaseFactory.getMongoDatabase("def_gfs");
 ​
         // 创建GridFSBucket 并指定文件系统使用的bucket
         return GridFSBuckets.create(db,"def_bucket");
     }
 ​
     /**
      * 下载、预览文件
      * @param fileId String 文件的objectId
      * @param response HttpServletResponse
      */
     @RequestMapping("/download/{fid}")
     public void download(@PathVariable("fid") String fileId, HttpServletResponse response) throws IOException {
         if (null == fileId || fileId.isEmpty() || !ObjectId.isValid(fileId)){
             response.setStatus(HttpStatus.NOT_FOUND.value());
             response.getWriter().println("File id is empty or not object id.");
             return;
         }
 ​
         // 拿到bucket
         GridFSBucket gridFsBucket = defaultGridFsBucket(this.mongoTemplate);
 ​
         ObjectId objectId = new ObjectId(fileId);
         // 查询文件
         GridFSFile file = gridFsBucket.find(new Document("_id", objectId)).first();
 ​
         // 没有查询到文件
         if (null == file){
             response.setStatus(HttpStatus.NOT_FOUND.value());
             response.getWriter().println("File not found.");
             return;
         }
 ​
 ​
         response.setContentType("application/octet-stream");
         // 下载文件能正常显示中文
         response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getFilename(), "UTF-8"));
 ​
         // 写出到response
         gridFsBucket.downloadToStream(objectId,response.getOutputStream());
 ​
     }
 ​
 }
 ​

4.2 文件下载测试

浏览器根据返回可以正常打开文件保存窗口

image-20220605193529404.png

文件可以正常打开

image-20220605193613806.png

4.3 视频预览出现问题

当前的文件下载和文件预览没有任何问题,但是却在小程序的视频预览中出现问题,以下是在开发者工具中进行调试,没有发生问题,

image-20220605194701746.png

但是却在手机端预览的时候频繁报错,而且视频打不开。以下分别是手机端的预览效果和后台的控制台报错。

小程序GridFS不显示bug-minipro.jpg

出现这个错误的原因是微信小程序默认用分段流读取的方式来加载视频,而我们当前的接口是一次性返回的

GridFS小程序bug.gif

4.4 改造文件下载

使用**ResourceHttpRequestHandler来处理文件下载,重写内部的protected Resource getResource(HttpServletRequest request)** 方法,自定义返回**GridFsResource**来处理下载

4.4.1 GridFsResourceHttpRequestHandler

 import com.mongodb.client.gridfs.GridFSDownloadStream;
 import com.mongodb.client.gridfs.model.GridFSFile;
 import org.springframework.core.io.Resource;
 import org.springframework.data.mongodb.gridfs.GridFsResource;
 import org.springframework.http.converter.ResourceHttpMessageConverter;
 import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
 import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
 ​
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 ​
 /**
  * @author WangKangSheng
  * @date 2022-06-05 20:05
  */
 public class GridFsResourceHttpRequestHandler extends ResourceHttpRequestHandler {
 ​
     private final GridFSFile file;
     private final GridFSDownloadStream stream;
 ​
     public GridFsResourceHttpRequestHandler(GridFSFile file, GridFSDownloadStream stream) {
         this.file = file;
         this.stream = stream;
         // 这两行代码必须有 否则会报 Not initialized 错误
         this.setResourceRegionHttpMessageConverter(new ResourceRegionHttpMessageConverter());
         this.setResourceHttpMessageConverter(new ResourceHttpMessageConverter());
     }
 ​
     @Override protected Resource getResource(HttpServletRequest request) throws IOException {
         return new GridFsResource(file,stream);
     }
 ​
 }

4.4.2 修改后的文件下载接口

 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.gridfs.GridFSBucket;
 import com.mongodb.client.gridfs.GridFSBuckets;
 import com.mongodb.client.gridfs.GridFSDownloadStream;
 import com.mongodb.client.gridfs.model.GridFSFile;
 import com.yjntc.demo.gfs.config.GridFsResourceHttpRequestHandler;
 import org.bson.Document;
 import org.bson.types.ObjectId;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.mongodb.MongoDatabaseFactory;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 ​
 /**
  *  文件上传下载
  * @author WangKangSheng
  * @date 2022-06-05 18:07
  */
 @RestController
 @RequestMapping("/file")
 public class FileController {
 ​
     @Autowired
     private MongoTemplate mongoTemplate;
 ​
     /**
      * 获得默认gridFS bucket,
      *  指定上传文件的库为 'def_gfs'
      *   这样做的好处是,多个系统使用不同的数据库连接,可以通过指定统一的数据库来创建GridFSBucket
      * @param mongoTemplate MongoTemplate
      * @return GridFSBucket
      */
     private GridFSBucket defaultGridFsBucket(MongoTemplate mongoTemplate){
         if (null == mongoTemplate){
             throw new NullPointerException("获取默认的GridFSBucket时mongoTemplate不能为空");
         }
         // 通过mongoTemplate来获取默认的数据库连接
         MongoDatabaseFactory mongoDatabaseFactory = mongoTemplate.getMongoDatabaseFactory();
         MongoDatabase db = mongoDatabaseFactory.getMongoDatabase("def_gfs");
 ​
         // 创建GridFSBucket 并指定文件系统使用的bucket
         return GridFSBuckets.create(db,"def_bucket");
     }
 ​
     /**
      * 下载、预览文件
      * @param fileId String 文件的objectId
      * @param response HttpServletResponse
      */
     @RequestMapping("/download/{fid}")
     public void download(@PathVariable("fid") String fileId, HttpServletResponse response, HttpServletRequest request) throws IOException, ServletException {
         if (null == fileId || fileId.isEmpty() || !ObjectId.isValid(fileId)){
             response.setStatus(HttpStatus.NOT_FOUND.value());
             response.getWriter().println("File id is empty or not object id.");
             return;
         }
 ​
         // 拿到bucket
         GridFSBucket gridFsBucket = defaultGridFsBucket(this.mongoTemplate);
 ​
         ObjectId objectId = new ObjectId(fileId);
         // 查询文件
         GridFSFile file = gridFsBucket.find(new Document("_id", objectId)).first();
 ​
         // 没有查询到文件
         if (null == file){
             response.setStatus(HttpStatus.NOT_FOUND.value());
             response.getWriter().println("File not found.");
             return;
         }
 ​
         GridFSDownloadStream downloadStream = gridFsBucket.openDownloadStream(objectId);
 ​
         // 使用自定义的ResourceHttpRequestHandler
         GridFsResourceHttpRequestHandler gridFsResourceHttpRequestHandler = new GridFsResourceHttpRequestHandler(file, downloadStream);
         gridFsResourceHttpRequestHandler.handleRequest(request,response);
 ​
     }
 ​
 }

修改后的接口在小程序,浏览器等都能正常使用了