本节,我们将介绍如何基于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文件服务器
- 下篇文章给大家讲解的是分布式系统开发实战: 分布式监控,分布式监控常用技术;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!