SpringBoot 使用Mongo的GridFs實現(xiàn)分布式文件存儲操作
前言
這段時間在公司實習,安排給我一個任務,讓在系統(tǒng)里實現(xiàn)一個知識庫的模塊,產品說,就像百度網盤那樣。。。我tm…,這不就是應了那句話,“這個需求很簡單,怎么實現(xiàn)我不管”。
可是我google小能手怎么會認輸呢,本來還說研究一下FastDFS啥的,但是因為我們項目用的Mongo作為數(shù)據庫,了解到Mongo自帶分布式文件系統(tǒng)GridFs,這簡直天助我也。
GridFs介紹
什么時候使用GridFs
我們平時使用Mongo也可以直接把文件的二進制保存在Document中,就相當于mysql的blob格式,但是mongo限制Document最大為16MB,那我們超過16MB的文件可咋辦吶,就可以利用GridFs來存儲。
- 在某些情況下,在MongoDB數(shù)據庫中存儲大文件可能比在系統(tǒng)級文件系統(tǒng)上更高效。
- 如果文件系統(tǒng)限制目錄中的文件數(shù),則可以使用GridFS根據需要存儲任意數(shù)量的文件。如果要從大型文件的各個部分訪問信息而無需將整個文件加載到內存中,可以使用GridFS調用文件的各個部分,而無需將整個文件讀入內存。
- 如果要保持文件和元數(shù)據在多個系統(tǒng)和設施中自動同步和部署,可以使用GridFS。使用地理位置分散的副本集時,MongoDB可以自動將文件及其元數(shù)據分發(fā)到多個 mongod實例和工具中。
GridFs的原理
GridFS不是將文件存儲在單個文檔中,而是將文件分成多個部分或塊,并將每個塊存儲為單獨的文檔。
GridFS使用兩個集合來存儲文件。一個集合存儲文件塊,另一個存儲文件元數(shù)據。

默認情況下,每一個塊的大小為255kB; 但最后一個塊除外。最后一個塊只有必要的大小。類似地,不大于塊大小的文件只有最終塊,只使用所需的空間和一些額外的元數(shù)據。
當查詢GridFS文件時,驅動程序將根據需要重新組裝塊??梢詫νㄟ^GridFS存儲的文件執(zhí)行范圍查詢。還可以從文件的任意部分訪問信息,例如“跳過”到視頻或音頻文件的中間。
環(huán)境
- Spring Boot 2.0.4
- Maven 3.5
- Java 1.8
- MongoDB 4.0
- Robo 1.3.1
引入依賴和項目配置
首先添加Mongo客戶端的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
然后編寫配置文件
spring:
application:
name: demo
data:
mongodb:
uri: mongodb://root:你的密碼@localhost:27019
authentication-database: admin
database: 你的數(shù)據庫
使用GridFsTemplate操作GridFs
GridFsTemplate是Spring提供的專門操作GridFs的客戶端,提供了一系列開箱即用的方法

只要把它注入到我們的Conteoller中,就可以愉快的CRUD了,需要注意的是獲取文件時要注入MongoDbFactory ,我們使用默認配置的話,直接注入就好。
import com.mongodb.Block;
import com.mongodb.MongoClient;
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 com.mongodb.client.gridfs.model.GridFSUploadOptions;
import com.mongodb.gridfs.GridFS;
import com.mongodb.gridfs.GridFSDBFile;
import org.bson.BsonValue;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
@RestController
@RequestMapping("/files")
public class FileController {
private Logger logger = LoggerFactory.getLogger(GridFsServiceImpl.class);
//匹配文件ID的正則
private static Pattern NUMBER_PATTERN = Pattern.compile("(?<==).*(?=})");
@Autowired
GridFsTemplate gridFsTemplate;
@Autowired
MongoDbFactory mongoDbFactory;
/**
* 上傳文件
*
* @param file 文件
* @return 文件名和文件存儲的fileId鍵值對的Map
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public Map<String, ObjectId> upload(@RequestParam(value = "file") MultipartFile file) {
if(pathId == null || fileType == null || pathId.equals("") || fileType.equals("")){
logger.debug("上傳文件出錯");
throw new RuntimeException("上傳文件出錯:pathId and fileType can not be null");
}
Map<String, String> map = new HashMap<>(1);
String fileName = file.getOriginalFilename();
//設置元數(shù)據
DBObject metaData = new BasicDBObject();
metaData.put("userId","1");
metaData.put("fileExtension",FilenameUtils.getExtension(file.getOriginalFilename()));
//存儲文件的擴展名
try {
InputStream inputStream = file.getInputStream();
ObjectId fileId = gridFsTemplate.store(inputStream, fileName,metaData);
//這個getFileId是我自己封裝的獲取文件ID的方法
map.put(file.getOriginalFilename(),getFileId(fileId.toString()));
} catch (IOException e) {
logger.debug("上傳文件失敗: "+file);
throw new RuntimeException("上傳文件失?。?+e.getMessage());
}
return map;
}
/**
* 通過文件fileId下載文件
*
* @param fileId 文件fileId
* @param response 文件輸出流
*/
@RequestMapping(value = "/downLoadByFileId")
public void downLoadByFileId(@RequestParam(value = "fileId") ObjectId fileId, HttpServletResponse response) {
GridFSFile gridFsFile = gridFsTemplate.findOne(new Query().addCriteria(Criteria.where("_id").is(fileId)));
if (gridFsFile != null) {
// mongo-java-driver3.x以上的版本就變成了這種方式獲取
GridFSBucket bucket = GridFSBuckets.create(mongoDbFactory.getDb());
GridFSDownloadStream gridFsDownloadStream = bucket.openDownloadStream(gridFsFile.getObjectId());
GridFsResource gridFsResource = new GridFsResource(gridFsFile,gridFsDownloadStream);
String fileName = gridFsFile.getFilename().replace(",", "");
//處理中文文件名亂碼
if (request.getHeader("User-Agent").toUpperCase().contains("MSIE") ||
request.getHeader("User-Agent").toUpperCase().contains("TRIDENT")
|| request.getHeader("User-Agent").toUpperCase().contains("EDGE")) {
fileName = java.net.URLEncoder.encode(fileName, "UTF-8");
} else {
//非IE瀏覽器的處理:
fileName = new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
}
response.setHeader("Content-Disposition", "inline;filename=\"" + fileName + "\"");
response.setContentType(application/octet-stream);
IOUtils.copy(gridFsResource.getInputStream(), response.getOutputStream());
}else {
logger.info("文件不存在");
}
}
/**
* 根據文件名查詢文件列表
* @param fileName 文件名
* @return
*/
@RequestMapping("/search")
public List<Map<String,Object>> search(String filePath, String fileName) {
//這里返回的是一個List<Map<String,Object>>
//因為GridFSFindIterable 迭代出來的GridFSFile不能直接返回
GridFSFindIterable gridFSFiles = gridFsTemplate.find(new Query().addCriteria
(Criteria.where("filename").regex("^.*"+fileName+".*$")));
return getGridFSFiles(gridFSFiles);
}
/**
* 通過fileId刪除文件
* @param fileId 文件ID
* @return 成功為true, 失敗為false
*/
@RequestMapping("/deleteFilesByObjectId")
public boolean deleteFilesByObjectId(@RequestParam(value = "fileId") String fileId) {
try {
gridFsTemplate.delete(new Query().addCriteria(Criteria.where("_id").is(fileId)));
}catch (Exception e){
logger.debug("刪除文件失敗: fileId= "+fileId);
throw new RuntimeException("刪除文件失敗:"+e.getMessage());
}
}
/**
* 根據GridFSFiles迭代器返回文件列表,因為不能直接返回,所以寫了這個工具方法
* @param gridFSFiles 從數(shù)據庫取出的文件集合
* @return List<Map<String,Object>>
*/
private List<Map<String,Object>> getGridFSFiles(GridFSFindIterable gridFSFiles){
List<Map<String,Object>> result = new ArrayList<>();
for (GridFSFile file : gridFSFiles) {
HashMap<String,Object> map = new HashMap<>(6);
map.put("fileId",getFileId(file.getId().toString()));
map.put("fileName",file.getFilename());
map.put("fileExtension",file.getMetadata().get("fileExtension"));
map.put("fileSize",file.getLength()/1024);
map.put("uploadTime",file.getUploadDate());
map.put("user",file.getMetadata().get("userId"));
result.add(map);
}
return result;
}
/**
* 因為從mongo中獲取的文件Id是BsonObjectId {value=5d7068bbcfaf962be4c7273f}的樣子
* 需要字符串截取
* @param bsonObjectId 數(shù)據庫文件的BsonObjectId
*/
private String getFileId(String bsonObjectId) {
Matcher m = NUMBER_PATTERN.matcher(bsonObjectId);
if(!m.find()){
return bsonObjectId;
}
return m.group();
}
}
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
火遍全網的Hutool使用Builder模式創(chuàng)建線程池的方法
這篇文章主要介紹了火遍全網的Hutool使用Builder模式創(chuàng)建線程池的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
簡單捋捋@RequestParam 和 @RequestBody的使用
這篇文章主要介紹了簡單捋捋@RequestParam 和 @RequestBody的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12
Spring-AOP 靜態(tài)正則表達式方法如何匹配切面
這篇文章主要介紹了Spring-AOP 靜態(tài)正則表達式方法如何匹配切面的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07

