烟妄狄菌 發表於 2020-4-6 13:38:00

SpringBoot学习笔记(十一:使用MongoDB存储文件 )

<br>
<p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>一、MongoDB存储文件<ul><li>1、MongoDB存储小文件</li><li>2、MongoDB存储大文件<ul><li>2.1、GridFS存储原理</li><li>2.2、GridFS使用<ul><li>2.2.1、使用shell命令</li><li>2.2.2、使用API</li></ul></li></ul></li></ul></li><li>二、SpringBoot整合MongoDB存储文件<ul><li>1、MongoDB存储小文件<ul><li>1.1、添加依赖</li><li>1.2、配置</li><li>1.3、模型层</li><li>1.4、持久层</li><li>1.5、服务层</li><li>1.6、控制层</li><li>1.7、工具类</li><li>1.8、前端页面</li><li>1.9、运行效果</li></ul></li><li>2、MongoDB存储大文件<ul><li>2.1、依赖</li><li>2.2、启动类</li><li>2.3、配置</li><li>2.4、实体类</li><li>2.5、服务层</li><li>2.6、控制层</li><li>2.7、运行效果</li></ul></li></ul></li></ul></div><p></p>
<br>
<blockquote>
<p>感谢各位的阅读。博主的这篇文章只是博主的学习笔记,有些问题博主也不太清楚,所以没有解答,抱歉。 源码比较乱,大家将就着看:https://gitee.com/fighter3/dairly-learn.git</p>
</blockquote>
<br>
<h1 id="一mongodb存储文件">一、MongoDB存储文件</h1>
<br>
<h2 id="1mongodb存储小文件">1、MongoDB存储小文件</h2>
<p>MongoDB是一个面向文档的数据库,使用BSON(Binary JSON:二进制JSON)格式来存储数据。</p>
<br>
<center><font face="仿宋">BSON格式</font></center>
<p><img src="https://img-blog.csdnimg.cn/20200405154058875.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<br>
<p>BSON支持在一个文档中最多存储16MB的二进制数据。如果存储的是小于16M的文件,可以直接将文件转换为二进制数据,以文档形式存入集合。</p>
<p>Java中文件和二进制转换也比较简单:</p>
<ul>
<li>文件转换为byte数组</li>
</ul>
<pre><code class="language-java">public static byte[] fileToByte(File file) throws IOException{
    byte[] bytes = null;
    FileInputStream fis = null;
    try{
      fis = new FileInputStream(file);
      bytes = new bytes[(int) file.length()];
      fis.read(bytes);
    }catch(IOException e){
      e.printStackTrace();
      throw e;
    }finally{
      fis.close();
    }
    return bytes;
}
</code></pre>
<ul>
<li>byte数组转换为文件</li>
</ul>
<pre><code class="language-java">
public static void bytesToFile(byte[] bFile, String fileDest) {

      FileOutputStream fileOuputStream = null;

      try {
            fileOuputStream = new FileOutputStream(fileDest);
            fileOuputStream.write(bFile);

      } catch (IOException e) {
            e.printStackTrace();
      } finally {
            if (fileOuputStream != null) {
                try {
                  fileOuputStream.close();
                } catch (IOException e) {
                  e.printStackTrace();
                }
            }
      }

    }
</code></pre>
<p>如果是实现文件下载功能,可以把字节码直接写进流中。</p>
<br>
<h2 id="2mongodb存储大文件">2、MongoDB存储大文件</h2>
<p>MongoDB单个文档的存储限制是16M,如果要存储大于16M的文件,就要用到MongoDB GridFS。</p>
<p>GridFS是Mongo的一个子模块,使用GridFS可以基于MongoDB来持久存储文件。并且支持分布式应用(文件分布存储和读取)。作为MongoDB中二进制数据存储在数据库中的解决方案,通常用来处理大文件。</p>
<p>GridFS不是MongoDB自身特性,只是一种将大型文件存储在MongoDB的文件规范,所有官方支持的驱动均实现了GridFS规范。GridFS制定大文件在数据库中如何处理,通过开发语言驱动来完成、通过API接口来存储检索大文件。</p>
<br>
<h3 id="21gridfs存储原理">2.1、GridFS存储原理</h3>
<p>GridFS使用两个集合(collection)存储文件。一个集合是chunks, 用于存储文件内容的二进制数据;一个集合是files,用于存储文件的元数据。</p>
<p>GridFS会将两个集合放在一个普通的buket中,并且这两个集合使用buket的名字作为前缀。MongoDB的GridFs默认使用fs命名的buket存放两个文件集合。因此存储文件的两个集合分别会命名为集合fs.files ,集合fs.chunks。</p>
<p>当把一个文件存储到GridFS时,如果文件大于chunksize (每个chunk块大小为256KB),会先将文件按照chunk的大小分割成多个chunk块,最终将chunk块的信息存储在fs.chunks集合的多个文档中。然后将文件信息存储在fs.files集合的唯一一份文档中。其中fs.chunks集合中多个文档中的file_id字段对应fs.files集中文档”_id”字段。</p>
<p>读文件时,先根据查询条件在files集合中找到对应的文档,同时得到“_id”字段,再根据“_id”在chunks集合中查询所有“files_id”等于“_id”的文档。最后根据“n”字段顺序读取chunk的“data”字段数据,还原文件。</p>
<br>
<center><font face="仿宋">GridFS存储过程</font></center>
<p><img src="https://img-blog.csdnimg.cn/20200405170439568.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
<p>fs.files 集合存储文件的元数据,以类json格式文档形式存储。每在GridFS存储一个文件,则会在fs.files集合中对应生成一个文档。</p>
<p><br></p><center><font face="仿宋">fs.files集合中文档的存储内容</font></center><p></p>
<p><img src="https://img-blog.csdnimg.cn/20200405170953357.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<br>
<p>fs.chunks 集合存储文件文件内容的二进制数据,以类json格式文档形式存储。每在GridFS存储一个文件,GridFS就会将文件内容按照chunksize大小(chunk容量为256k)分成多个文件块,然后将文件块按照类json格式存在.chunks集合中,每个文件块对应fs.chunk集合中一个文档。一个存储文件会对应一到多个chunk文档。</p>
<p><br></p><center><font face="仿宋"> fs.chunks集合中文档的存储内容</font></center><p></p>
<p><img src="https://img-blog.csdnimg.cn/20200405171310215.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<br>
<h3 id="22gridfs使用">2.2、GridFS使用</h3>
<br>
<h4 id="221使用shell命令">2.2.1、使用shell命令</h4>
<p>mongoDB提供mingofiles工具,可以使用命令行来操作GridFS。其实有四个主要命令,分别为:</p>
<ul>
<li>put —存储命令</li>
<li>get —获取命令</li>
<li>list —列表命令</li>
<li>delete —删除命令</li>
</ul>
<p>操作实例:</p>
<ul>
<li>存储文件<br>
向数据库中存储文件的格式:<font color="blue">mongofiles -d 数据库名字-l"要上传的文件的完整路径名"put"上传后的文件名"</font></li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200405173203395.png?#pic_center" alt="在这里插入图片描述" loading="lazy"> 在filetest数据库中就会多出2个集合,它们存储了GridFS文件系统的所有文件信息,查询这两个集合就能看到上传的文件的一些信息:</p>
<p><img src="https://img-blog.csdnimg.cn/20200405173857140.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
<ul>
<li>列出文件<br>
查看GridFS文件系统中所有文件:<font color="blue">mongofiles -d 数据库名字 list</font></li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200405174118288.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
<ul>
<li>获取文件<br>
从GridFS文件系统中下载一个文件到本地:<font color="blue">mongofiles -d 数据库名字 -l "将文件保存在本地的完整路径名" get "GridFS文件系统中的文件名"</font> ,如果不写-l以及后面的路径参数,则保存到当前位置。</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200405175029403.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br></p>
<ul>
<li>删除文件<br>
删除GridFS文件系统中的某个文件:<font color="blue">mongofiles -d 数据库名字 delete" 文件名 "</font></li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200405175534546.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br></p>
<h4 id="222使用api">2.2.2、使用API</h4>
<p>MongoDB支持多种编程语言驱动。比如c、java、C#、nodeJs等。因此可以使用这些语言MongoDB驱动API操作,扩展GridFS。</p>
<p>以Java为例:</p>
<ul>
<li>
<p>依赖包和版本:<br>
org.mongodb:3.2.2<br>
mongo-java-driver:3.2.2</p>
</li>
<li>
<p>公共方法</p>
</li>
</ul>
<pre><code class="language-java">public MongoDatabase mongoDatabase() throws Exception{
    MongoClient mongoClient = new MongoClient(mongoHost, mongoPort);
    return mongoClient.getDatabase(dbName);
}
</code></pre>
<ul>
<li>上传文件</li>
</ul>
<pre><code class="language-java">@Test
public void upload() throws Exception {
    // 获取文件流
    File file = new File("E:/zookeeper-3.4.8.tar.gz");
    InputStream in = new FileInputStream(file);
    // 创建一个容器,传入一个`MongoDatabase`类实例db
    GridFSBucket bucket = GridFSBuckets.create(mongoDatabase());
    // 上传
    ObjectId fileId = bucket.uploadFromStream(UUID.randomUUID().toString(), in);
    System.out.println("上传完成。 文件ID:"+fileId);
}
</code></pre>
<ul>
<li>查找文件</li>
</ul>
<pre><code class="language-java">@Test
public void findOne() throws Exception {
    // 获取文件ID
    String objectId = "57fbaffcec773716ecc54ef4";
    // 创建一个容器,传入一个`MongoDatabase`类实例db
    GridFSBucket bucket = GridFSBuckets.create(mongoDatabase());
    // 获取内容
    GridFSFindIterable gridFSFindIterable = bucket.find(Filters.eq("_id", new ObjectId(objectId)));
    GridFSFile gridFSFile = gridFSFindIterable.first();
    System.out.println("filename: " + gridFSFile.getFilename());
}
</code></pre>
<ul>
<li>下载文件</li>
</ul>
<pre><code class="language-java">@Test
public void download() throws Exception {
    // 获取文件ID
    String objectId = "57fbaffcec773716ecc54ef4";
    // 获取文件流
    File file = new File("D:/zookeeper-3.4.8.tar.gz");
    // 创建一个容器,传入一个`MongoDatabase`类实例db
    GridFSBucket bucket = GridFSBuckets.create(mongoDatabase());
    // 创建输出流
    OutputStream os = new FileOutputStream(file);
    // 下载
    bucket.downloadToStream(new ObjectId(objectId), os);
    System.out.println("下载完成。");
}
</code></pre>
<ul>
<li>删除文件</li>
</ul>
<pre><code class="language-java">@Test
public void delete() throws Exception {
    // 获取文件ID
    String objectId = "57fbaffcec773716ecc54ef4";
    // 创建一个容器,传入一个`MongoDatabase`类实例db
    GridFSBucket bucket = GridFSBuckets.create(mongoDatabase());
    // 删除
    bucket.delete(new ObjectId(objectId));
    System.out.println("删除完成。");
}
</code></pre>
<br>
<h1 id="二springboot整合mongodb存储文件">二、SpringBoot整合MongoDB存储文件</h1>
<p>MongoDB可以将文件直接存储在文档或者通过GridFS存储大文件,这里同样进行SpringBoot整合MongoDB的两种实现。</p>
<br>
<h2 id="1mongodb存储小文件-1">1、MongoDB存储小文件</h2>
<p>SpringBoot整合MongoDB将文件以文档形式直接存入集合,和普通的MongDB存储区别不大。</p>
<h3 id="11添加依赖">1.1、添加依赖</h3>
<ul>
<li>spring-boot-starter-data-mongodb:用来操作MongoDB</li>
<li>spring-boot-starter-thymeleaf:前端页面采用thymeleaf模板</li>
</ul>
<pre><code class="language-java">       &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-mongodb&lt;/artifactId&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
      &lt;/dependency&gt;
</code></pre>
<br>
<h3 id="12配置">1.2、配置</h3>
<pre><code class="language-java">server.address=localhost
server.port=8081

# thymeleaf配置,开发环境不启用缓存,正式环境下请启用缓存,提高性能
spring.thymeleaf.cache=false
# thymeleaf对html元素格式要求严格,设置它的mode为HTML,忘记结束标签后不会报错
spring.thymeleaf.mode=HTML

# 编码
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true

# MongoDB 配置
# 连接url
spring.data.mongodb.uri=mongodb://test:test@localhost:27017/filetest


# 文件上传限制
spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=50MB
</code></pre>
<br>
<h3 id="13模型层">1.3、模型层</h3>
<p>文件模型类:</p>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2020/1/11
* @Description 文档类
*/
@Document
public class FileModel {

    @Id// 主键
    private String id;
    private String name; // 文件名称
    private String contentType; // 文件类型
    private long size;
    private Date uploadDate;
    private String md5;
    private Binary content; // 文件内容
    private String path; // 文件路径

   /**
   *省略getter/setter
   */
    protected FileModel() {
    }


    public FileModel(String name, String contentType, long size,Binary content) {
      this.name = name;
      this.contentType = contentType;
      this.size = size;
      this.content = content;
    }


    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof FileModel)) return false;
      FileModel fileModel = (FileModel) o;
      return size == fileModel.size &amp;&amp;
                Objects.equals(id, fileModel.id) &amp;&amp;
                Objects.equals(name, fileModel.name) &amp;&amp;
                Objects.equals(contentType, fileModel.contentType) &amp;&amp;
                Objects.equals(uploadDate, fileModel.uploadDate) &amp;&amp;
                Objects.equals(md5, fileModel.md5) &amp;&amp;
                Objects.equals(content, fileModel.content) &amp;&amp;
                Objects.equals(path, fileModel.path);
    }

    @Override
    public int hashCode() {
      return Objects.hash(id, name, contentType, size, uploadDate, md5, content, path);
    }
}
</code></pre>
<br>
<h3 id="14持久层">1.4、持久层</h3>
<p>采用MongoRepository的方式操作MongoDB,只需在接口里继承MongoRepository,就可以调用MongoRepository的内置方法。</p>
<pre><code class="language-java">public interface FileRepository extends MongoRepository&lt;FileModel,String&gt; {
}
</code></pre>
<br>
<h3 id="15服务层">1.5、服务层</h3>
<p>服务层接口:</p>
<pre><code class="language-java">public interface FileService {
    /**
   * 保存文件
   */
    FileModel saveFile(FileModel file);

    /**
   * 删除文件
   */
    void removeFile(String id);

    /**
   * 根据id获取文件
   */
    Optional&lt;FileModel&gt; getFileById(String id);

    /**
   * 分页查询,按上传时间降序
   * @return
   */
    List&lt;FileModel&gt; listFilesByPage(int pageIndex, int pageSize);
}
</code></pre>
<p>服务层实现类:</p>
<pre><code class="language-java">@Service
public class FileServiceImpl implements FileService {
    @Autowired
    private FileRepository fileRepository;

    @Override
    public FileModel saveFile(FileModel file) {
      return fileRepository.save(file);
    }

    @Override
    public void removeFile(String id) {
      fileRepository.deleteById(id);
    }

    @Override
    public Optional&lt;FileModel&gt; getFileById(String id) {
      return fileRepository.findById(id);
    }

    @Override
    public List&lt;FileModel&gt; listFilesByPage(int pageIndex, int pageSize) {
      Page&lt;FileModel&gt; page = null;
      List&lt;FileModel&gt; list = null;
      Sort sort = Sort.by(Sort.Direction.DESC,"uploadDate");
      Pageable pageable = PageRequest.of(pageIndex, pageSize, sort);
      page = fileRepository.findAll(pageable);
      list = page.getContent();
      return list;
    }
}
</code></pre>
<br>
<h3 id="16控制层">1.6、控制层</h3>
<pre><code class="language-java">@Controller
public class FileController {
    @Autowired
    private FileService fileService;

    @Value("${server.address}")
    private String serverAddress;

    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/")
    public String index(Model model) {
      // 展示最新二十条数据
      model.addAttribute("files", fileService.listFilesByPage(0, 20));
      return "index";
    }

    /**
   * 分页查询文件
   */
    @GetMapping("files/{pageIndex}/{pageSize}")
    @ResponseBody
    public List&lt;FileModel&gt; listFilesByPage(@PathVariable int pageIndex, @PathVariable int pageSize) {
      return fileService.listFilesByPage(pageIndex, pageSize);
    }

    /**
   * 获取文件片信息
   */
    @GetMapping("files/{id}")
    @ResponseBody
    public ResponseEntity&lt;Object&gt; serveFile(@RequestParam("id") String id) throws UnsupportedEncodingException {

      Optional&lt;FileModel&gt; file = fileService.getFileById(id);

      if (file.isPresent()) {
            return ResponseEntity.ok()
                  .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=" + new String(file.get().getName().getBytes("utf-8"),"ISO-8859-1"))
                  .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
                  .header(HttpHeaders.CONTENT_LENGTH, file.get().getSize() + "").header("Connection", "close")
                  .body(file.get().getContent().getData());
      } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not fount");
      }

    }

    /**
   * 在线显示文件
   */
    @GetMapping("/view")
    @ResponseBody
    public ResponseEntity&lt;Object&gt; serveFileOnline(@RequestParam("id") String id) {

      Optional&lt;FileModel&gt; file = fileService.getFileById(id);

      if (file.isPresent()) {
            return ResponseEntity.ok()
                  .header(HttpHeaders.CONTENT_DISPOSITION, "fileName=\"" + file.get().getName() + "\"")
                  .header(HttpHeaders.CONTENT_TYPE, file.get().getContentType())
                  .header(HttpHeaders.CONTENT_LENGTH, file.get().getSize() + "").header("Connection", "close")
                  .body(file.get().getContent().getData());
      } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not fount");
      }

    }

    /**
   * 上传
   */
    @PostMapping("/")
    public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {

      try {
            FileModel f = new FileModel(file.getOriginalFilename(), file.getContentType(), file.getSize(),
                  new Binary(file.getBytes()));
            f.setMd5(MD5Util.getMD5(file.getInputStream()));
            fileService.saveFile(f);
            System.out.println(f);
      } catch (IOException | NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "Your " + file.getOriginalFilename() + " is wrong!");
            return "redirect:/";
      }

      redirectAttributes.addFlashAttribute("message",
                "You successfully uploaded " + file.getOriginalFilename() + "!");

      return "redirect:/";
    }

    /**
   * 上传接口
   */
    @PostMapping("/upload")
    @ResponseBody
    public ResponseEntity&lt;String&gt; handleFileUpload(@RequestParam("file") MultipartFile file) {
      FileModel returnFile = null;
      try {
            FileModel f = new FileModel(file.getOriginalFilename(), file.getContentType(), file.getSize(),
                  new Binary(file.getBytes()));
            f.setMd5(MD5Util.getMD5(file.getInputStream()));
            returnFile = fileService.saveFile(f);
            String path = "//" + serverAddress + ":" + serverPort + "/view/" + returnFile.getId();
            return ResponseEntity.status(HttpStatus.OK).body(path);

      } catch (IOException | NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
      }

    }

    /**
   * 删除文件
   */
    @GetMapping("/delete")
    @ResponseBody
    public ResponseEntity&lt;String&gt; deleteFile( @RequestParam("id") String id) {

      try {
            fileService.removeFile(id);
            return ResponseEntity.status(HttpStatus.OK).body("DELETE Success!");
      } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
      }
    }
}
</code></pre>
<br>
<h3 id="17工具类">1.7、工具类</h3>
<p>md5工具类:</p>
<pre><code class="language-java">public class MD5Util {
    /**
   * 获取该输入流的MD5值
   */
    public static String getMD5(InputStream is) throws NoSuchAlgorithmException, IOException {
      StringBuffer md5 = new StringBuffer();
      MessageDigest md = MessageDigest.getInstance("MD5");
      byte[] dataBytes = new byte;

      int nread = 0;
      while ((nread = is.read(dataBytes)) != -1) {
            md.update(dataBytes, 0, nread);
      };
      byte[] mdbytes = md.digest();

      // convert the byte to hex format
      for (int i = 0; i &lt; mdbytes.length; i++) {
            md5.append(Integer.toString((mdbytes &amp; 0xff) + 0x100, 16).substring(1));
      }
      return md5.toString();
    }
}
</code></pre>
<br>
<h3 id="18前端页面">1.8、前端页面</h3>
<p>前端页面index.html:</p>
<pre><code class="language-java">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th="http://www.thymeleaf.org"&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1 style="text-align: center"&gt;文件服务&lt;/h1&gt;
&lt;br&gt;
&lt;div &gt;
    &lt;a href="/"&gt;首页&lt;/a&gt;
&lt;/div&gt;

&lt;br&gt;&lt;br&gt;
&lt;div th:if="${message}" style="margin-left: 10%"&gt;
    &lt;h2 th:text="${message}"/&gt;
&lt;/div&gt;

&lt;br&gt;
&lt;div &gt;
    &lt;form method="POST" enctype="multipart/form-data" action="/"&gt;
      &lt;table&gt;
            &lt;tr&gt;&lt;td&gt;上传文件:&lt;/td&gt;&lt;td&gt;&lt;input type="file" name="file" /&gt;&lt;/td&gt;&lt;/tr&gt;
            &lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;input type="submit" value="上传" /&gt;&lt;/td&gt;&lt;/tr&gt;
      &lt;/table&gt;

    &lt;/form&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;

&lt;div style="margin-left: 5%"&gt;
    &lt;h3 style="text-align: center"&gt;文件列表&lt;/h3&gt;
    &lt;table border="1"&gt;
      &lt;thead&gt;
      &lt;tr style="background-color: beige"&gt;
            &lt;td&gt;文件名&lt;/td&gt;
            &lt;td&gt;文件ID&lt;/td&gt;
            &lt;td&gt;contentType&lt;/td&gt;
            &lt;td&gt;文件大小&lt;/td&gt;
            &lt;td&gt;上传时间&lt;/td&gt;
            &lt;td&gt;md5&lt;/td&gt;
            &lt;td&gt;操作&lt;/td&gt;
      &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
      &lt;tr th:if="${files.size()} eq 0"&gt;
            &lt;td colspan="3"&gt;没有文件信息!!&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr th:each="file : ${files}"&gt;
            &lt;td&gt;&lt;a th:href="'files/'+${file.id}" th:text="${file.name}" /&gt;&lt;/td&gt;
            &lt;td th:text="${file.id}" &gt;&lt;/td&gt;
            &lt;td th:text="${file.contentType}" &gt;&lt;/td&gt;
            &lt;td th:text="${file.size}" &gt;&lt;/td&gt;
            &lt;td th:text="${file.uploadDate}" &gt;&lt;/td&gt;
            &lt;td th:text="${file.md5}" &gt;&lt;/td&gt;
            &lt;td&gt;&lt;a target="_blank" th:href="@{/view(id=${file.id})}"&gt;预览&lt;/a&gt;|&lt;a th:href="@{/delete(id=${file.id})}"&gt;删除&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
&lt;/div&gt;

&lt;/body&gt;

&lt;style&gt;
    body{
      text-align: center;
    }

    table{
      margin: auto;
    }

&lt;/style&gt;

</code></pre>
<br>
<h3 id="19运行效果">1.9、运行效果</h3>
<ul>
<li>上传文件:</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/2020040522124817.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<p><img src="https://img-blog.csdnimg.cn/20200405221318895.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<br>
<ul>
<li>
<p>预览<br>
<img src="https://img-blog.csdnimg.cn/20200405221436343.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
</li>
<li>
<p>下载</p>
</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200405222202148.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<br>
<ul>
<li>删除<br>
<img src="https://img-blog.csdnimg.cn/20200405222336173.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></li>
</ul>
<p>在文件的操作过程中,可以通过可视化工具或shell来查看存储在MongoDB中的文件:</p>
<ul>
<li>可以看到,在fileModel集合中存储了我们上传的文件,文件的内容是以二进制的形式存储</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/2020040522574615.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
<h2 id="2mongodb存储大文件-1">2、MongoDB存储大文件</h2>
<p>Spring Data MongoDB提供了GridFsOperations接口以及相应的实现GridFsTemplate,可以和GridFs交互。</p>
<p>这里在上一个工程的基础上进行改造。</p>
<br>
<h3 id="21依赖">2.1、依赖</h3>
<p>和上一个工程相比,这里添加开源工具包hutool的依赖:</p>
<pre><code class="language-java">         &lt;dependency&gt;
            &lt;groupId&gt;cn.hutool&lt;/groupId&gt;
            &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
            &lt;version&gt;4.5.1&lt;/version&gt;
      &lt;/dependency&gt;
</code></pre>
<br>
<h3 id="22启动类">2.2、启动类</h3>
<pre><code class="language-java">@SpringBootApplication
public class SpringbootFileGridfsApplication {

    public static void main(String[] args) {
      SpringApplication.run(SpringbootFileGridfsApplication.class, args);
    }

    //Tomcat large file upload connection reset
    @Bean
    public TomcatServletWebServerFactory tomcatEmbedded() {
      TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
      tomcat.addConnectorCustomizers((TomcatConnectorCustomizer) connector -&gt; {
            if ((connector.getProtocolHandler() instanceof AbstractHttp11Protocol&lt;
                  ?&gt;)) {
                //-1 means unlimited
                ((AbstractHttp11Protocol&lt;?&gt;) connector.getProtocolHandler()).setMaxSwallowSize(-1);
            }
      });
      return tomcat;
    }
}
</code></pre>
<p>TomcatServletWebServerFactory() ⽅法主要是为了解决上传文件较大时出现连接重置的问题,这个异常后台是捕捉不到的:</p>
<p><img src="https://img-blog.csdnimg.cn/2020040610553223.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<h3 id="23配置">2.3、配置</h3>
<ul>
<li>application.properties</li>
</ul>
<pre><code class="language-java"># MongoDB 配置
# 连接uri
#spring.data.mongodb.uri=mongodb://test:test@localhost:27017/filetest
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=filetest
spring.data.mongodb.username=test
spring.data.mongodb.password=test

# 文件上传限制
spring.servlet.multipart.max-file-size=1020MB
spring.servlet.multipart.max-request-size=1020MB
</code></pre>
<ul>
<li>配置类</li>
</ul>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2020/1/11
* @Description
*/
@Configuration
public class MongoConfig {
    //获取配置文件中数据库信息
    @Value("${spring.data.mongodb.database}")
    String db;

    ////GridFSBucket用于打开下载流
    @Bean
    public GridFSBucket getGridFSBucket(MongoClient mongoClient){
      MongoDatabase mongoDatabase = mongoClient.getDatabase(db);
      GridFSBucket bucket = GridFSBuckets.create(mongoDatabase);
      return bucket;
    }
}
</code></pre>
<br>
<h3 id="24实体类">2.4、实体类</h3>
<ul>
<li>文件实体类</li>
</ul>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2020/1/11
* @Description
*/
@Document
public class FileDocument {
    @Id// 主键
    private String id;
    private String name;      // 文件名称
    private long size;          // 文件大小
    private Date uploadDate;    // 上传时间
    private String md5;         // 文件MD5值
    private byte[] content;   // 文件内容
    private String contentType; // 文件类型
    private String suffix;      // 文件后缀名
    private String description; // 文件描述
    private String gridfsId;    // 大文件管理GridFS的ID
   
   /**
   * 省略getter、setter、equales、hashCode、toString方法
   */
}
</code></pre>
<ul>
<li>接口结果实体类</li>
</ul>
<pre><code class="language-java">**
* @Author 三分恶
* @Date 2020/1/11
* @Description 公用数据返回模型
*/
public class ResponseModel {
    public static final String Success = "success";
    public static final String Fail = "fail";

    private String code = "fail";
    private String message = "";
    private String data;

    //私有构造函数,此类不允许手动实例化,需要调用getInstance()获取实例
    private ResponseModel() {
    }

    /**
   * 返回默认的实例
   * @return
   */
    public static ResponseModel getInstance() {
      ResponseModel model = new ResponseModel();
      model.setCode(ResponseModel.Fail);
      return model;
    }
   
   /**
   *省略getter/setter
   */

}
</code></pre>
<br>
<h3 id="25服务层">2.5、服务层</h3>
<p>上一个实例里采用MongReposity来操作MongoDB,这里操作MongoDB采用MongoTemplate,操作GridFs采用GridFsTemplate。</p>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2020/1/11
* @Description
*/
@Service
public class FileServiceImpl implements FileService {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private GridFsTemplate gridFsTemplate;

    @Autowired
    private GridFSBucket gridFSBucket;


    /**
   * 保存文件
   * @param file
   * @return
   */
    @Override
    public FileDocument saveFile(FileDocument file) {
      file = mongoTemplate.save(file);
      return file;
    }

    /**
   * 上传文件到Mongodb的GridFs中
   * @param in
   * @param contentType
   * @return
   */
    @Override
    public String uploadFileToGridFS(InputStream in , String contentType){
      String gridfsId = IdUtil.simpleUUID();
      //将文件存储进GridFS中
      gridFsTemplate.store(in, gridfsId , contentType);
      return gridfsId;
    }


    /**
   * 删除文件
   * @param id
   */
    @Override
    public void removeFile(String id) {
      //根据id查询文件
      FileDocument fileDocument = mongoTemplate.findById(id , FileDocument.class );
      if(fileDocument!=null){
            //根据文件ID删除fs.files和fs.chunks中的记录
            Query deleteFileQuery = new Query().addCriteria(Criteria.where("filename").is(fileDocument.getGridfsId()));
            gridFsTemplate.delete(deleteFileQuery);
            //删除集合fileDocment中的数据
            Query deleteQuery=new Query(Criteria.where("id").is(id));
            mongoTemplate.remove(deleteQuery,FileDocument.class);
      }
    }

    /**
   * 根据id查看文件
   * @param id
   * @return
   */
    @Override
    public Optional&lt;FileDocument&gt; getFileById(String id){
      FileDocument fileDocument = mongoTemplate.findById(id , FileDocument.class );
      if(fileDocument != null){
            Query gridQuery = new Query().addCriteria(Criteria.where("filename").is(fileDocument.getGridfsId()));
            try {
                //根据id查询文件
                GridFSFile fsFile = gridFsTemplate.findOne(gridQuery);
                //打开流下载对象
                GridFSDownloadStream in = gridFSBucket.openDownloadStream(fsFile.getObjectId());
                if(in.getGridFSFile().getLength() &gt; 0){
                  //获取流对象
                  GridFsResource resource = new GridFsResource(fsFile, in);
                  //获取数据
                  fileDocument.setContent(IoUtil.readBytes(resource.getInputStream()));
                  return Optional.of(fileDocument);
                }else {
                  fileDocument = null;
                  return Optional.empty();
                }
            }catch (IOException ex){
                ex.printStackTrace();
            }
      }
      return Optional.empty();
    }


    /**
   * 分页列出文件
   * @param pageIndex
   * @param pageSize
   * @return
   */
    @Override
    public List&lt;FileDocument&gt; listFilesByPage(int pageIndex, int pageSize) {
      Query query = new Query().with(Sort.by(Sort.Direction.DESC, "uploadDate"));
      long skip = (pageIndex -1) * pageSize;
      query.skip(skip);
      query.limit(pageSize);
      Field field = query.fields();
      field.exclude("content");
      List&lt;FileDocument&gt; files = mongoTemplate.find(query , FileDocument.class );
      return files;

    }
}

</code></pre>
<br>
<h3 id="26控制层">2.6、控制层</h3>
<p>控制层变动不大,主要是调用服务层方法的返回值和参数有变化:</p>
<pre><code class="language-java">@Controller
public class FileController {
    @Autowired
    private FileService fileService;

    @Value("${server.address}")
    private String serverAddress;

    @Value("${server.port}")
    private String serverPort;

    @RequestMapping(value = "/")
    public String index(Model model) {
      // 展示最新二十条数据
      model.addAttribute("files", fileService.listFilesByPage(0, 20));
      return "index";
    }

    /**
   * 分页查询文件
   */
    @GetMapping("files/{pageIndex}/{pageSize}")
    @ResponseBody
    public List&lt;FileDocument&gt; listFilesByPage(@PathVariable int pageIndex, @PathVariable int pageSize) {
      return fileService.listFilesByPage(pageIndex, pageSize);
    }

    /**
   * 获取文件片信息
   */
    @GetMapping("files/{id}")
    @ResponseBody
    public ResponseEntity&lt;Object&gt; serveFile(@PathVariable String id) throws UnsupportedEncodingException {

      Optional&lt;FileDocument&gt; file = fileService.getFileById(id);

      if (file.isPresent()) {
            return ResponseEntity.ok()
                  .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=" + new String(file.get().getName().getBytes("utf-8"),"ISO-8859-1"))
                  .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
                  .header(HttpHeaders.CONTENT_LENGTH, file.get().getSize() + "").header("Connection", "close")
                  .body(file.get().getContent());
      } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not fount");
      }

    }

    /**
   * 在线显示文件
   */
    @GetMapping("/view")
    @ResponseBody
    public ResponseEntity&lt;Object&gt; serveFileOnline(@RequestParam("id") String id) {
      Optional&lt;FileDocument&gt; file = fileService.getFileById(id);
      if (file.isPresent()) {
            return ResponseEntity.ok()
                  .header(HttpHeaders.CONTENT_DISPOSITION, "fileName=" + file.get().getName())
                  .header(HttpHeaders.CONTENT_TYPE, file.get().getContentType())
                  .header(HttpHeaders.CONTENT_LENGTH, file.get().getSize() + "").header("Connection", "close")
                  .header(HttpHeaders.CONTENT_LENGTH , file.get().getSize() + "")
                  .body(file.get().getContent());
      } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not found");
      }


    }

    /**
   * 上传
   */
    @PostMapping("/")
    public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {

      try {
            FileDocument fileDocument = new FileDocument();
            fileDocument.setName(file.getOriginalFilename());
            fileDocument.setSize(file.getSize());
            fileDocument.setContentType(file.getContentType());
            fileDocument.setUploadDate(new Date());
            String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
            fileDocument.setSuffix(suffix);
            fileDocument.setMd5(MD5Util.getMD5(file.getInputStream()));
            //将文件存入gridFs
            String gridfsId = fileService.uploadFileToGridFS(file.getInputStream() , file.getContentType());
            fileDocument.setGridfsId(gridfsId);
            fileDocument = fileService.saveFile(fileDocument);
            System.out.println(fileDocument);
      } catch (IOException | NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "Your " + file.getOriginalFilename() + " is wrong!");
            return "redirect:/";
      }

      redirectAttributes.addFlashAttribute("message",
                "You successfully uploaded " + file.getOriginalFilename() + "!");

      return "redirect:/";
    }

    /**
   * 上传接口
   */
    @PostMapping("/upload")
    @ResponseBody
    public ResponseEntity&lt;String&gt; handleFileUpload(@RequestParam("file") MultipartFile file) {
      FileDocument returnFile = null;
      try {
            FileDocument fileDocument = new FileDocument();
            fileDocument.setName(file.getOriginalFilename());
            fileDocument.setSize(file.getSize());
            fileDocument.setContentType(file.getContentType());
            fileDocument.setUploadDate(new Date());
            String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
            fileDocument.setSuffix(suffix);
            fileDocument.setMd5(MD5Util.getMD5(file.getInputStream()));
            //将文件存入gridFs
            String gridfsId = fileService.uploadFileToGridFS(file.getInputStream() , file.getContentType());
            fileDocument.setGridfsId(gridfsId);
            returnFile = fileService.saveFile(fileDocument);
            String path = "//" + serverAddress + ":" + serverPort + "/view/" + returnFile.getId();
            return ResponseEntity.status(HttpStatus.OK).body(path);

      } catch (IOException | NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
      }

    }

    /**
   * 删除文件
   */
    @GetMapping("/delete")
    @ResponseBody
    public ResponseModel deleteFile( @RequestParam("id") String id) {
      ResponseModel model = ResponseModel.getInstance();
      if(!StrUtil.isEmpty(id)){
            fileService.removeFile(id);
            model.setCode(ResponseModel.Success);
            model.setMessage("删除成功");
      }else {
            model.setMessage("请传入文件id");
      }
      return model;
    }
}

</code></pre>
<p><br><br></p>
<ul>
<li>前端页面没有变动。</li>
</ul>
<br>
<h3 id="27运行效果">2.7、运行效果</h3>
<ul>
<li>
<p>上传文件<br>
这里我们选择一个比较大的mp4文件<br>
<img src="https://img-blog.csdnimg.cn/20200406131658193.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><img src="https://img-blog.csdnimg.cn/20200406131732487.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
</li>
<li>
<p>预览<br>
预览还存在问题,后台会报错:<font color="orange">org.springframework.http.converter.HttpMessageNotWritableException: No converter for with preset Content-Type 'video/mp4'</font><br>
待解决</p>
</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200406132120539.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
<ul>
<li>
<p>下载<br>
<img src="https://img-blog.csdnimg.cn/20200406132210138.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
</li>
<li>
<p>删除<br>
<img src="https://img-blog.csdnimg.cn/20200406132727723.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
</li>
</ul>
<br>
<p><b> 在上传和删除数据的过程中,可以通过可视化工具或shell来查看MongoDB中的数据</b></p>
<ul>
<li>fileDocment中的数据:fileDocment是一个普通的集合,对应地以文档的形式存储了FileDocument实例</li>
</ul>
<p><img src="https://img-blog.csdnimg.cn/20200406132404627.png?#pic_center" alt="在这里插入图片描述" loading="lazy"></p>
<ul>
<li>
<p>fs.files中的数据:文件的元数据<br>
<img src="https://img-blog.csdnimg.cn/20200406132549470.png?#pic_center" alt="在这里插入图片描述" loading="lazy"><br>
<br></p>
</li>
<li>
<p>fs.chunks中的数据:file被切分成若干个chunks,存储了文件的二进制数据<br>
<img src="https://img-blog.csdnimg.cn/20200406132636574.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpbmF0XzQwNzcwNjU2,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述" loading="lazy"></p>
</li>
</ul>
<p><br><br></p>
<br>
<hr>
<p><font face="仿宋" size="3">本文为学习笔记类博客,学习资料来源见参考!</font></p>
<p><br><br></p>
<p>【1】:MongoDB GridFS<br>
【2】:Mongodb的文件存储GridFs<br>
【3】:MongoDB学习笔记(五) MongoDB文件存取操作<br>
【4】:《MongoDB大数据权威处理指南》<br>
【5】:java文件转二进制<br>
【6】:Java将文件转为字节数组<br>
【7】:java文件下载的几种方式 <br>
【8】:文件和byte数组之间相互转换<br>
【9】:关于知名数据库MongoDB,有个功能你不可不知!<br>
【10】:MongoDB 学习笔记(五):固定集合、GridFS文件系统与服务器端脚本<br>
【11】:GridFS 基于 MongoDB 的分布式文件存储系统<br>
【12】:SpringBoot Mongodb文件存储服务器<br>
【13】:MongoDB文件服务器搭建<br>
【14】:基于 MongoDB 及 Spring Boot 的文件服务器的实现<br>
【15】:SpringBoot中使用GridFS<br>
【16】:SpringBoot2.x集成mongoDB4.0实现音频文件的上传下载功能<br>
【17】:10.18. GridFS Support<br>
【18】:GridFS in Spring Data MongoDB<br>
【19】:纯洁的微笑 《精通SpringBoot 42讲》<br>
【20】:JAVA 应用 / hutool / hutool系列教材 (一)- 介绍 - 简介</p><br><br>
来源:https://www.cnblogs.com/three-fighter/p/12641771.html
頁: [1]
查看完整版本: SpringBoot学习笔记(十一:使用MongoDB存储文件 )