基於 MongoDB 及 Spring Boot 的文件服務器的實現
MongoDB 是一個介於關係數據庫和非關係數據庫之間的產品,是非關係數據庫當中功能最豐富,最像關係數據庫的,旨在為 WEB 應用提供可擴展的高性能數據存儲解決方案。它支持的數據結構非常鬆散,是類似 JSON 的 BSON 格式,因此可以存儲比較複雜的數據類型。
本文將介紹通過 MongoDB 存儲二進製文件,從而實現一個文件服務器 MongoDB File Server。
文件服務器的需求
本文件服務器致力於小型文件的存儲,比如博客中圖片、普通文檔等。由於MongoDB 支持多種數據格式的存儲,對於二進製的存儲自然也是不話下,所以可以很方便的用於存儲文件。由於 MongoDB 的 BSON 文檔對於數據量大小的限製(每個文檔不超過16M),所以本文件服務器主要針對的是小型文件的存儲。對於大型文件的存儲(比如超過16M),MongoDB 官方已經提供了成熟的產品 GridFS,讀者朋友可以自行了解。
本文不會對 MongoDB 的概念、基本用法做過多的介紹,有興趣的朋友可自行查閱其他文獻,比如,筆者所著的《分布式係統常用技術及案例分析》一書,對 MongoDB 方麵也有所著墨。
所需環境
本例子采用的開發環境如下:
- MongoDB 3.4.4
- Spring Boot 1.5.3.RELEASE
- Thymeleaf 3.0.3.RELEASE
- Thymeleaf Layout Dialect 2.2.0
- Embedded MongoDB 2.0.0
- Gradle 3.5
其中,Spring Boot 用於快速構建一個可獨立運行的 Java 項目;Thymeleaf 作為前端頁麵模板,方便展示數據;Embedded MongoDB 則是一款由 Organization Flapdoodle OSS 出品的內嵌 MongoDB,可以在不啟動 MongoDB 服務器的前提下,方麵進行相關的 MongoDB 接口測試;Gradle 是一個類似於 Apache Maven 概念的新一代項目自動化構建工具。
有關 Spring Boot 的方麵的內容,可以參閱筆者所著著的開源書《Spring Boot 教程》。有關 Thymeleaf 的方麵的內容,可以參閱筆者所著著的開源書《Thymeleaf 教程》。有關 Gradle 的方麵的內容,可以參閱筆者所著著的開源書《Gradle 3 用戶指南》。
build.gradle
本文所演示的項目,是采用 Gradle 進行組織以及構建的,如果您對 Gradle 不熟悉,也可以自行將項目轉為 Maven 項目。
build.gradle 文件內容如下:
// buildscript 代碼塊中腳本優先執行
buildscript {
// ext 用於定義動態屬性
ext {
springBootVersion = '1.5.3.RELEASE'
}
// 自定義 Thymeleaf 和 Thymeleaf Layout Dialect 的版本
ext['thymeleaf.version'] = '3.0.3.RELEASE'
ext['thymeleaf-layout-dialect.version'] = '2.2.0'
// 自定義 Embedded MongoDB 的 依賴
ext['embedded-mongo.version'] = '2.0.0'
// 使用了 Maven 的中央倉庫(你也可以指定其他倉庫)
repositories {
//mavenCentral()
maven {
url 'https://maven.aliyun.com/nexus/content/groups/public/'
}
}
// 依賴關係
dependencies {
// classpath 聲明說明了在執行其餘的腳本時,ClassLoader 可以使用這些依賴項
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
// 使用插件
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
// 打包的類型為 jar,並指定了版本
version = '1.0.0'
// 指定編譯 .java 文件的 JDK 版本
sourceCompatibility = 1.8
// 默認使用了 Maven 的中央倉庫。這裏改用自定義的鏡像庫
repositories {
//mavenCentral()
maven {
url 'https://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 文件中的各配置項的注釋已經非常詳盡了,這裏就不再贅述其配置項的含義了。
領域對象
文檔類 File
文檔類是類似與 JPA 中的實體的概念。
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 byte[] content; // 文件內容
private String path; // 文件路徑
...
// getter/setter
...
protected File() {
}
public File(String name, String contentType, long size,byte[] 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 中的文檔概念。
存儲庫 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> {
}
服務接口及實現類
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.delete(id);
}
@Override
public File getFileById(String id) {
return fileRepository.findOne(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 = new PageRequest(pageIndex, pageSize, sort);
page = fileRepository.findAll(pageable);
list = page.getContent();
return list;
}
}
控製層/API 資源層
FileController 控製器作為 API 的提供者,接收用戶的請求及響應。API 的定義符合 RESTful 的風格。有關 REST 相關的知識,讀者可以參閱筆者所著的開源書《[REST 實戰]》(https://github.com/waylau/rest-in-action)。
@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) {
// 展示最新二十條數據
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) {
File file = fileService.getFileById(id);
if (file != null) {
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=\"" + file.getName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, "application/octet-stream" )
.header(HttpHeaders.CONTENT_LENGTH, file.getSize()+"")
.header("Connection", "close")
.body( file.getContent());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not fount");
}
}
/**
* 在線顯示文件
* @param id
* @return
*/
@GetMapping("/view/{id}")
@ResponseBody
public ResponseEntity<Object> serveFileOnline(@PathVariable String id) {
File file = fileService.getFileById(id);
if (file != null) {
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "fileName=\"" + file.getName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, file.getContentType() )
.header(HttpHeaders.CONTENT_LENGTH, file.getSize()+"")
.header("Connection", "close")
.body( file.getContent());
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not fount");
}
}
/**
* 上傳
* @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(), 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(),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 可以被跨域請求。為了能夠啟用該注解,仍然需要安全配置類的支持。
安全配置
為了支持跨域請求,我們設置了安全配置類 SecurityConfig:
@Configuration
@EnableWebMvc
public class SecurityConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*") ; // 允許跨域請求
}
}
運行
有多種方式可以運行 Gradle 的 Java 項目。使用 Spring Boot Gradle Plugin 插件運行是較為簡便的一種方式,隻需要執行:
$ gradlew bootRun
其他運行方式,請參閱筆者的開源書《Spring Boot 教程》
項目成功運行後,通過瀏覽器訪問 https://localhost:8081 即可。首頁提供了上傳的演示界麵,上傳後,就能看到上傳文件的詳細信息:
相關上傳的接口暴露在了 https://localhost:8081/ ,其中
- GET /files/{pageIndex}/{pageSize} : 分頁查詢已經上傳了的文件
- GET /files/{id} : 下載某個文件
- GET /view/{id} : 在線預覽某個文件。比如,顯示圖片
- POST /upload : 上傳文件
- DELETE /{id} : 刪除文件
源碼
MongoDB File Server 是一款開源的產品,完整的項目源碼見 https://github.com/waylau/mongodb-file-server。
參考文獻
- 原文同步至:https://waylau.com/mogodb-file-server-with-spring-boot/
- 《https://docs.mongodb.com/manual/core/gridfs/
- 《分布式係統常用技術及案例分析》:https://github.com/waylau/distributed-systems-technologies-and-cases-analysis
- 《Spring Boot 教程》:https://github.com/waylau/spring-boot-tutorial
- 《Thymeleaf 教程》:https://github.com/waylau/thymeleaf-tutorial
- 《Gradle 3 用戶指南》:https://github.com/waylau/gradle-3-user-guide
- 《REST 實戰》https://github.com/waylau/rest-in-action
- https://github.com/waylau/mongodb-file-server
最後更新:2017-06-14 01:32:11