閱讀576 返回首頁    go 阿裏雲 go 技術社區[雲棲]


基於 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

參考文獻

最後更新:2017-06-14 01:32:11

  上一篇:go  Feign入門書目錄
  下一篇:go  建網站選擇網絡誠信專屬.xin域名,誠信贏天下!