實戰:基於MongoDB文件服務器
本節,我們將介紹如何基於MongoDB技術來存儲二進制文件,從而實現一個文件服務器MongoDB File Server。
文件服務器的需求
本文件服務器致力於小型文件的存儲,比如博客中的圖片、普通文檔等。由於MongoDB支持多種數據格式的存儲,對於二進制的存儲自然也是不在話下,所以可以很方便地用於存儲文件。由於MongoDB的BSON文檔對於數據量大小的限制(每個文檔不超過16MB),所以本文件服務器主要針對的是小型文件的存儲。對於大型文件的存儲(比如超過16MB),MongoDB官方已經提供了成熟的產品GridFS,讀者朋友可以自行了解。
文件服務器應能夠提供與平台無關的REST API供外部系統調用。
文件服務器整體的API設計如下。
·GET/files/{pageIndex}/{pageSize}:分頁查詢已經上傳了的文件。
·GET/files/{id}:下載某個文件。
·GET/view/{id}:在線預覽某個文件。比如,顯示圖片。
·POST/upload:上傳文件。
·DELETE/{id}:刪除文件。
我們創建一個新項目,稱之為mongodb-file-server。
所需技術
本例子採用的開發技術如下。
·MongoDB 3.4.6。·Spring Boot 2.0.0.M2。
·Spring Data Mongodb 2.0.0.M4。
·Thymeleaf 3.0.6.RELEASE。
·Thymeleaf Layout Dialect 2.2.2。
·Embedded MongoDB 2.0.0。
其中,Spring Boot用於快速構建一個可獨立運行的Java項目;
Thymeleaf作為前端頁面模板,方便展示數據;Embedded MongoDB則是一款由Organization Flapdoodle OSS出品的內嵌MongoDB,可以在不啟動MongoDB服務器的前提下,方便進行相關的MongoDB接口測試。
本文所演示的項目,是採用Gradle進行組織以及構建的,如果對Gradle不熟悉,也可以自行將項目轉為Maven項目。
build.gradle文件完整配置內容如下。
buildscript { // buildscript 代碼塊中腳本優先執行
// ext 用於定義動態屬性
ext {
springBootVersion = '2.0.0.M2'
}
// 使用了 Maven 的中央倉庫(也可以指定其他倉庫)
repositories {
//mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
}
// 依賴關係
dependencies {
// classpath 聲明說明了在執行其餘的腳本時,ClassLoader 可以使用這些依賴項
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBoot
Version}")
}
}
// 使用插件
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
// 指定了生成的編譯文件的版本,默認是打成了 jar 包
version = '1.0.0'
// 指定編譯 .java 文件的 JDK 版本
sourceCompatibility = 1.8
// 使用了 Maven 的中央倉庫(也可以指定其他倉庫)
repositories {//mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
}
// 依賴關係
dependencies {
// 該依賴用於編譯階段
compile('org.springframework.boot:spring-boot-starter-web')
// 添加 Thymeleaf 的依賴
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
// 添加 Spring Data Mongodb 的依賴
compile('org.springframework.boot:spring-boot-starter-data-mongodb')
// 添加 Embedded MongoDB 的依賴用於測試
compile('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
// 該依賴用於測試階段
testCompile('org.springframework.boot:spring-boot-starter-test')
}
該build.gradle文件中的各配置項的注釋已經非常詳盡了,這裡就不再贅述其配置項的含義了。
文件服務器的實現
在mongodb-file-server項目基礎上,我們將實現文件服務器的功能。
1.領域對象
首先,我們要對文件服務器進行建模。相關的領域模型如下。
文檔類是類似與JPA中的實體的概念。不同的是JPA是採用@Entity註解,而文檔類是採用@Document註解。
在
com.waylau.spring.boot.fileserver.domain包下,我們創建了一個File類。
import org.bson.types.Binary;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
...
@Document
public class File {
@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 File() {
}
public File(String name, String contentType, long size,Binary content) {
this.name = name;
this.contentType = contentType;
this.size = size;
this.uploadDate = new Date();
this.content = content;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
File fileInfo = (File) object;
return java.util.Objects.equals(size, fileInfo.size)
&& java.util.Objects.equals(name, fileInfo.name)
&& java.util.Objects.equals(contentType, fileInfo.contentType)
&& java.util.Objects.equals(uploadDate, fileInfo.uploadDate)
&& java.util.Objects.equals(md5, fileInfo.md5)
&& java.util.Objects.equals(id, fileInfo.id);
}
@Override
public int hashCode() {
return java.util.Objects.hash(name, contentType, size, uploadDate, md5, id);
}
@Override
public String toString() {
return "File{"
+ "name='" + name + '''
+ ", contentType='" + contentType + '''
+ ", size=" + size
+ ", uploadDate=" + uploadDate
+ ", md5='" + md5 + '''
+ ", id='" + id + '''
+ '}';
}
}
需要注意以下兩點。
·文檔類,主要採用的是Spring Data MongoDB中的註解,用於標識這是NoSQL中的文檔概念。
·文件的內容,我們是用org.bson.types.Binary類型來進行存儲。
2.存儲庫FileRepository
存儲庫用於提供與數據庫“打交道”的常用的數據訪問接口。其中FileRepository接口繼承自
org.springframework.data.mongodb.repository.MongoRepository即可,無須自行實現該接口的功能,Spring Data MongoDB會自動實現接口中的方法。
import org.springframework.data.mongodb.repository.MongoRepository;
import com.waylau.spring.boot.fileserver.domain.File;
public interface FileRepository extends MongoRepository<File, String> {
}
3.服務接口及實現類
FileService接口定義了對於文件的CURD操作,其中查詢文件接口是採用的分頁處理,以有效提升查詢性能。
public interface FileService {
/**
* 保存文件
* @param File
* @return
*/
File saveFile(File file);
/**
* 刪除文件
* @param File
* @return
*/
void removeFile(String id);
/**
* 根據id獲取文件
* @param File
* @return
*/
File getFileById(String id);
/**
* 分頁查詢,按上傳時間降序* @param pageIndex
* @param pageSize
* @return
*/
List<File> listFilesByPage(int pageIndex, int pageSize);
}
FileServiceImpl實現了FileService中所有的接口。
@Service
public class FileServiceImpl implements FileService {
@Autowired
public FileRepository fileRepository;
@Override
public File saveFile(File file) {
return fileRepository.save(file);
}
@Override
public void removeFile(String id) {
fileRepository.deleteById(id);
}
@Override
public Optional<File> getFileById(String id) {
return fileRepository.findById(id);
}
@Override
public List<File> listFilesByPage(int pageIndex, int pageSize) {
Page<File> page = null;
List<File> list = null;
Sort sort = new Sort(Direction.DESC,"uploadDate");
Pageable pageable = PageRequest.of(pageIndex, pageSize, sort);
page = fileRepository.findAll(pageable);
list = page.getContent();
return list;
}
}
4.控制層、API資源層
FileController控制器作為API的提供者,接收用戶的請求及響應。
API的定義符合RESTful的風格。
@CrossOrigin(origins = "*", maxAge = 3600) // 允許所有域名訪問
@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) {
// 展示最新20條數據model.addAttribute("files", fileService.listFilesByPage(0, 20));
return "index";
}
/**
* 分頁查詢文件
*
* @param pageIndex
* @param pageSize
* @return
*/
@GetMapping("files/{pageIndex}/{pageSize}")
@ResponseBody
public List<File> listFilesByPage(@PathVariable int pageIndex,
@PathVariable int pageSize) {
return fileService.listFilesByPage(pageIndex, pageSize);
}
/**
* 獲取文件片信息
*
* @param id
* @return
*/
@GetMapping("files/{id}")
@ResponseBody
public ResponseEntity<Object> serveFile(@PathVariable String id) {
Optional<File> file = fileService.getFileById(id);
if (file.isPresent()) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=""
+ file.get().getName() + """)
.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
found");
}
}
/**
* 在線顯示文件
*
* @param id
* @return
*/
@GetMapping("/view/{id}")
@ResponseBody
public ResponseEntity<Object> serveFileOnline(@PathVariable String id) {
Optional<File> 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
found");
}
}
/**
* 上傳
*
* @param file
* @param redirectAttributes
* @return
*/
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
try {
File f = new File(file.getOriginalFilename(), file.getContentType(),
file.getSize(), new Binary(file.getBytes()));
f.setMd5(MD5Util.getMD5(file.getInputStream()));
fileService.saveFile(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:/";
}
/**
* 上傳接口
*
* @param file
* @return
*/
@PostMapping("/upload")
@ResponseBody
public ResponseEntity<String> handleFileUpload(@RequestParam("file") MultipartFile
file) {
File returnFile = null;
try {
File f = new File(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());
}
}
/*** 刪除文件
*
* @param id
* @return
*/
@DeleteMapping("/{id}")
@ResponseBody
public ResponseEntity<String> deleteFile(@PathVariable 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());
}
}
}
其中@CrossOrigin(origins=”*”,maxAge=3600)註解標識了API可以被跨域請求。
運行
有多種方式可以運行Gradle的Java項目。使用Spring Boot GradlePlugin插件運行是較為簡便的一種方式,只需要執行:
$ gradlew bootRun
項目成功運行後,通過瀏覽器訪問http://localhost:8081即可。如圖14-4所示,首頁提供了上傳的演示界面,上傳後,就能看到上傳文件的詳細信息。
圖14-4 上傳界面
其他配置項
我們打開application.properties配置文件,可以看到以下配置。
server.address=localhost
server.port=8081
# Thymeleaf
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML5
# limit upload file size
spring.http.multipart.max-file-size=1024KB
spring.http.multipart.max-request-size=1024KB
# independent MongoDB server
#spring.data.mongodb.uri=mongodb://localhost:27017/test
這些配置的含義如下。
·server.address和server.port用來指定文件服務器啟動的位置和端口號。
·
spring.http.multipart.max-file-size和spring.http.multipart.max-request-size用來限制上傳文件的大小,這裡設置最大是1MB。
·當spring.data.mongodb.uri沒有被指定的時候,默認會採用內嵌MongoDB服務器。如果要使用獨立部署的MongoDB服務器,那麼設置這個配置,並指定MongoDB服務器的地址。同時,將內嵌MongoDB的依賴注釋掉,操作如下。
dependencies {
//...
// 注釋掉內嵌的 MongoDB
// compile('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
//...
}
本文給大家講解的內容是分布式系統開發實戰: 分布式存儲,實戰:基於MongoDB文件服務器
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/255472.html