spring boot开发mongoDB做文档型数据库存储文档

本文详细介绍使用SpringBoot和MongoDB实现文件存储的步骤,包括依赖配置、实体类设计、服务接口及其实现,以及控制器代码。涵盖文件上传、下载、预览、删除等功能,并提供多模块项目环境下文件管理的解决方案。

首先这盘博客是参考SpringBoot Mongodb文件存储服务器,并且修改了原来博客中过时的废弃的方法,并把我实际做的公布出来。

一、首先提醒

因为我做的是多模块项目,用dubbozk实现的微服务,但是因为上传的文件过大,所以肯定不能通过微服务传输数据再到服务的实现类里去上传文件,所以我就在webapp(应该都知道是哪个模块)这个模块直接和mongoDB连接传输文件。

二、依赖(都是webapp模块下的)

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
 </dependency>

application.properties

spring.data.mongodb.uri=mongodb://user:123456@127.0.0.1:27017/first?authSource=admin&authMechanism=SCRAM-SHA-256
spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=50MB

三、用到的POJO

@Data
public class FileDocument implements Serializable {
    /**
     * id
     */
    private String id;
    /**
     * 文件名称
     */
    private String name;
    /**
     * 文件大小
     */
    private long size;
    /**
     * 上传时间
     */
    private Date uploadDate;
    /**
     * 文件的md5值
     */
    private String md5;
    /**
     * 文件内容
     */
    private byte[] content;
    /**
     * 文件类型
     */
    private String contentType;
    /**
     * 文件后缀
     */
    private String suffix;
    /**
     * 文件描述
     */
    private String description;
    /**
     * 大文件管理GridFS的ID
     */
    private String gridfsId;
}

四、服务和实现类

import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Optional;

/**
 * @author 孔超
 * @date 2020/5/11 15:20
 */
public interface FileService {
    /**
     * 保存信息的文件
     *
     * @param md5
     * @param file
     * @return
     */
    public FileDocument saveFile(String md5, MultipartFile file);

    /**
     * 移除文件
     *
     * @param id
     * @param isDeleteFile
     */
    public void removeFile(String id, boolean isDeleteFile);

    /**
     * 根据id查询附件
     *
     * @param id
     * @return
     */
    public Optional<FileDocument> getById(String id);

    /**
     * 根据md5获取文件对象
     *
     * @param md5
     * @return
     */
    public FileDocument getByMd5(String md5);

    /**
     * 文件分页
     *
     * @param pageIndex
     * @param pageSize
     * @return
     */
    public List<FileDocument> listFilesByPage(int pageIndex, int pageSize);
}

import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import com.mongodb.client.result.DeleteResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Field;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

/**
 * @author 孔超
 * @date 2020/5/11 15:18
 */
@Slf4j
@Service("fileService")
public class FileServiceImpl implements FileService {
    private static String collectionName = "fileDate";

    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private GridFsTemplate gridFsTemplate;
    @Autowired
    private GridFSBucket gridFSBucket;

    /**
     * 表单上传附件
     *
     * @param md5
     * @param file
     * @return
     */
    @Override
    public FileDocument saveFile(String md5, MultipartFile file) {
        //已存在该文件,则实现秒传
        FileDocument fileDocument = getByMd5(md5);
        if (fileDocument != null) {
            return fileDocument;
        }

        fileDocument = new FileDocument();
        fileDocument.setName(file.getOriginalFilename());
        fileDocument.setSize(file.getSize());
        fileDocument.setContentType(file.getContentType());
        fileDocument.setUploadDate(new Date());
        fileDocument.setMd5(md5);
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
        fileDocument.setSuffix(suffix);

        try {
            //文件存入gridfs
            String gridfsId = uploadFileToGridFS(file.getInputStream(), file.getContentType());
            fileDocument.setGridfsId(gridfsId);
            //上传的信息保存在mongodb
            fileDocument = mongoTemplate.save(fileDocument, collectionName);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return fileDocument;
    }

    /**
     * 上传文件到Mongodb的GridFs中
     *
     * @param in
     * @param contentType
     * @return
     */
    private String uploadFileToGridFS(InputStream in, String contentType) {
        String gridfsId = UUID.randomUUID().toString();
        //文件,存储在GridFS中
        gridFsTemplate.store(in, gridfsId, contentType);
        return gridfsId;
    }

    /**
     * 删除附件
     *
     * @param id           文件id
     * @param isDeleteFile 是否删除文件
     */
    @Override
    public void removeFile(String id, boolean isDeleteFile) {
        FileDocument fileDocument = mongoTemplate.findById(id, FileDocument.class, collectionName);
        if (fileDocument != null) {
            Query query = new Query().addCriteria(Criteria.where("_id").is(id));
            DeleteResult result = mongoTemplate.remove(query, collectionName);
            System.out.println("result:" + result.getDeletedCount());

            if (isDeleteFile) {
                Query deleteQuery = new Query().addCriteria(Criteria.where("filename").is(fileDocument.getGridfsId()));
                gridFsTemplate.delete(deleteQuery);
            }
        }
    }

    /**
     * 查询附件
     *
     * @param id 文件id
     * @return
     * @throws IOException
     */
    @Override
    public Optional<FileDocument> getById(String id) {
        FileDocument fileDocument = mongoTemplate.findById(id, FileDocument.class, collectionName);
        if (fileDocument != null) {
            Query gridQuery = new Query().addCriteria(Criteria.where("filename").is(fileDocument.getGridfsId()));
            try {
                GridFSFile fsFile = gridFsTemplate.findOne(gridQuery);
                GridFSDownloadStream in = gridFSBucket.openDownloadStream(fsFile.getObjectId());
                if (in.getGridFSFile().getLength() > 0) {
                    GridFsResource resource = new GridFsResource(fsFile, in);
                    fileDocument.setContent(inputStreamToByte(resource.getInputStream()));
                    return Optional.of(fileDocument);
                } else {
                    fileDocument = null;
                    return Optional.empty();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return Optional.empty();
    }

    /**
     * 流转换成byte数组
     *
     * @param inStream
     * @return
     * @throws IOException
     */
    private byte[] inputStreamToByte(InputStream inStream) throws IOException {
        ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
        //buff用于存放循环读取的临时数据
        byte[] buff = new byte[100];
        int rc = 0;
        while ((rc = inStream.read(buff, 0, 100)) > 0) {
            swapStream.write(buff, 0, rc);
        }
        return swapStream.toByteArray();
    }


    /**
     * 根据md5获取文件对象
     *
     * @param md5
     * @return
     */
    @Override
    public FileDocument getByMd5(String md5) {
        Query query = new Query().addCriteria(Criteria.where("md5").is(md5));
        return mongoTemplate.findOne(query, FileDocument.class, collectionName);
    }

    /**
     * 文件分页
     *
     * @param pageIndex
     * @param pageSize
     * @return
     */
    @Override
    public List<FileDocument> listFilesByPage(int pageIndex, int pageSize) {
        Sort sort = Sort.by(Sort.Order.desc("uploadDate"));
        Query query = new Query().with(sort);
        long skip = (pageIndex - 1) * pageSize;
        query.skip(skip);
        query.limit(pageSize);
        Field field = query.fields();
        field.exclude("content");
        List<FileDocument> files = mongoTemplate.find(query, FileDocument.class, collectionName);
        return files;
    }
}

五、FileController

1.返回值Result是我自己的工具类,用于返回信息的,你可以自己编
2.这里面和上传实现类是有重复的,我没整理,应该整理成工具类的
3.下面在线预览的前端需要进行处理,这里的简单例子不支持

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Optional;

/**
 * @author 孔超
 * @date 2020/5/11 15:17
 */
@Slf4j
@RestController
@RequestMapping("files")
public class FileController {
    @Resource
    FileService fileService;

    /**
     * 上传文件列表
     *
     * @param pageIndex
     * @param pageSize
     * @return
     */
    @RequestMapping("/list")
    public List<FileDocument> list(int pageIndex, int pageSize) {
        return fileService.listFilesByPage(pageIndex, pageSize);
    }

    /**
     * 在线显示文件:目前不能实现在线预览,需要前端进行处理,这个简单例子是不行
     *
     * @param id 文件id
     * @return
     */
    @GetMapping("/view")
    public ResponseEntity<Object> serveFileOnline(String id) {
        Optional<FileDocument> file = fileService.getById(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());
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not found");
        }
    }

    /**
     * 下载文件
     *
     * @param id
     * @return
     * @throws UnsupportedEncodingException
     */
    @GetMapping("/down")
    public ResponseEntity<Object> downloadFileById(String id) throws UnsupportedEncodingException {
        Optional<FileDocument> file = fileService.getById(id);
        if (file.isPresent()) {
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; fileName=" + URLEncoder.encode(file.get().getName(), "utf-8"))
                    .header(HttpHeaders.CONTENT_TYPE, "application/octet-stream")
                    .header(HttpHeaders.CONTENT_LENGTH, file.get().getSize() + "").header("Connection", "close")
                    .body(file.get().getContent());
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File was not found");
        }
    }

    /**
     * 表单上传文件
     * 当数据库中存在该md5值时,可以实现秒传功能
     *
     * @param file 文件
     * @return
     */
    @PostMapping("/upload")
    public Result formUpload(@RequestParam("file") MultipartFile file) {
        Result result = null;
        try {
            if (file != null && !file.isEmpty()) {
                String fileMd5 = getMD5(file.getInputStream());
                FileDocument fileDocument = fileService.saveFile(fileMd5, file);

                System.out.println(fileDocument);
                result = new Result(1, "上传成功", fileDocument.getId());
            } else {
                result = new Result(0, "请上传文件");
            }
        } catch (IOException e) {
            log.error("IOE异常", e);
            result = new Result(-1, "系统异常");
        } catch (Exception e) {
            log.error("上传文件系统异常,", e);
            result = new Result(-1, "系统异常");

        }
        return result;
    }

    /**
     * 删除附件
     *
     * @param id
     * @return
     */
    @GetMapping("/delete")
    public Result deleteFileByGetMethod(String id) {
        Result result = null;
        if (id != null) {
            fileService.removeFile(id, true);
            result = new Result(1, "删除成功");
        } else {
            result = new Result(0, "请传输文件");
        }
        return result;
    }

    /**
     * 流转换成MD5字符串
     *
     * @param inputStream
     * @return
     */
    private String getMD5(InputStream inputStream) {
        BigInteger md5 = null;
        try {
            byte[] buffer = inputStreamToByte(inputStream);
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(buffer, 0, buffer.length);
            byte[] b = md.digest();
            md5 = new BigInteger(1, b);
        } catch (NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
        }
        assert md5 != null;
        return md5.toString(16);
    }

    /**
     * 流转换成byte数组
     *
     * @param inStream
     * @return
     * @throws IOException
     */
    private byte[] inputStreamToByte(InputStream inStream) throws IOException {
        ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
        //buff用于存放循环读取的临时数据
        byte[] buff = new byte[100];
        int rc = 0;
        while ((rc = inStream.read(buff, 0, 100)) > 0) {
            swapStream.write(buff, 0, rc);
        }
        return swapStream.toByteArray();
    }

}

六 简单的例子

单独的Controller

这个和上面的FileController是不一样的,当然也可以在一起

 @RequestMapping("/gotoTest.do")
    public String gotoTest(Model model) {
    	//这里我定死了,没有根据前端提供的分页数据的来,这里的list是mongoDB里的数据的信息,
    	//如果想根据前端提供的数据来的话上面的参数加上@RequestParam(value = "pageIndex" , defaultValue = "1") int pageIndex,@RequestParam(value = "pageSize" , defaultValue = "10") int pageSize
        List<FileDocument> fileDocumentList = fileService.listFilesByPage(1, 10);
        model.addAttribute("fileDocumentList", fileDocumentList);
        return "text";
    }

    @RequestMapping("/upload")
    public String upload() {
        return "upload";
    }

test.ftl

我用的freemarker

 <div class="find-top1">
        <form action="" id="form_Recruitment">
            <table class="top-table">
                <tr>
                    <td>id</td>
                    <td>名字</td>
                    <td>大小</td>
                    <td>操作</td>
                </tr>
                <#list fileDocumentList as fileDocument>
                    <tr>
                        <td>${fileDocument.id}</td>
                        <td>${fileDocument.name}</td>
                        <td>${fileDocument.size}</td>
                        <td><a href="/files/view?id=${fileDocument.id}">预览</a><a>下载</a> <a>删除</a></td>
                    </tr>
                </#list>

            </table>
        </form>
    </div>

效果图

在这里插入图片描述

upload.ftl

<form method="POST" enctype="multipart/form-data" action="/files/upload">
    <p>
        文件:<input type="file" name="file"/>
    </p>
    <p>
        <input type="submit" value="上传"/>
    </p>
</form>

效果图

在这里插入图片描述
上面就是特别简单的实例

七 实际的例子

文档数据库就当好它的数据库,它记录的是文档,一般我们创建时很多时候是很多input和文件一起提交到后端,而关系型数据库记录的是文档在文档数据库中的id或者唯一标识符。
主要在于怎么提交既有文件又有input的form表单

前端:

<form name="form_recruiters">
            <table class="top-table">
                <tr>
                    <td class="top-table-label">姓名:</td>
                    <td><input type="text" name="name"></td>
                    <td class="top-table-label">应聘岗位:</td>
                    <td><input type="text" name="positions"></td>
                    <td class="top-table-label">工作类型:</td>
                    <td>
                        <select name="type">
                            <option>实习</option>
                            <option>全职</option>
                        </select>
                    </td>
                </tr>
                <tr>
                    <td class="top-table-label">上传简历</td>
                    <td colspan="5"><input type="file" name="file"></td>
                </tr>
                <tr>
                    <td colspan="6" style="text-align: center">
                        <i class="glyphicon glyphicon-edit"></i><input type="button" value="提交" data-dismiss="modal" onclick="submitRecruiters()"/>
                    </td>
                </tr>
            </table>
        </form>

js

   function submitRecruiters() {
   		//创建一个formdate对象传输
        var form = document.forms.namedItem("form_recruiters");
        var formData = new FormData(form);
        $.ajax({
            url: "/saveRecruiters.do",
            type: "POST",
            data: formData,
            dataType: "json",
            contentType: false, //必须
            processData: false, //必须
            cache: false,
            success: function (result) {
                if (result.code === 1) {
                    location.reload();
                }
                alert(result.msg);
            },
            error: function () {
                alert("连接服务器异常,请刷新后重试")
            }
        });
    }

后端

 @PostMapping("/saveRecruiters.do")
    @ResponseBody
    public Result saveRecruiters(@RequestParam("file") MultipartFile file, @Valid Recruiters recruiters) {
        if (file == null || file.isEmpty()) {
            return new Result(0, "请上传简历");
        }
        //uploadFile这个就是FileController中的上传文件的代码,返回的是文档数据库中的id
        String resumeId = uploadFile(file);
        if (resumeId == null) {
            return new Result(0, "简历上传失败,请重新上传");
        }
        recruiters.setResumeId(resumeId);
        Result result = null;
        try {
            Long isSuc = recruitersService.insert(recruiters);
            if (isSuc != 1) {
                result = new Result(0, "添加失败,请重新添加");
            } else {
                result = new Result(1, "添加成功");
            }
        } catch (Exception e) {
			 log.error("保存应聘者信息出现系统异常recruiters{}", JSON.toJSONString(recruiters), e);
            result = new Result(1,"出现系统异常");
        }
        return result;
    }

这样就完成了

1.了解Spring 2.了解NoSQL和文档数据库 3.要求 4.其他帮助资源 4.1。支持 4.1.1。社区论坛 4.1.2。专业支持 4.2。发展之后 5.新&值得注意的 5.1。Spring Data MongoDB 2.1中的新特性 5.2。Spring Data MongoDB 2.0中的新特性 5.3。Spring Data MongoDB 1.10中的新特性 5.4。Spring Data MongoDB 1.9中的新特性 5.5。Spring Data MongoDB 1.8中的新特性 5.6。Spring Data MongoDB 1.7中有什么新功能 6.依赖 6.1。Spring Boot的依赖管理 6.2。Spring框架 7.使用Spring Data Repositories 7.1。核心概念 7.2。查询方法 7.3。定义存储库接口 7.3.1。微调储存库定义 7.3.2。空处理存储库方法 7.3.3。将存储库与多个Spring Data模块一起使用 7.4。定义查询方法 7.4.1。查询查询策略 7.4.2。查询创建 7.4.3。属性表达式 7.4.4。特殊参数处理 7.4.5。限制查询结果 7.4.6。流式查询结果 7.4.7。异步查询结果 7.5。创建存储库实例 7.5.1。XML配置 7.5.2。JavaConfig 7.5.3。独立使用 7.6。Spring Data存储库的自定义实现 7.6.1。定制个人存储库 7.6.2。自定义基础存储库 7.7。从聚合根发布事件 7.8。Spring数据扩展 7.8.1。Querydsl扩展 7.8.2。Web支持 7.8.3。存储库填充程序 7.8.4。传统网络支持 参考文档 8.介绍 8.1。文档结构 9. MongoDB支持 9.1。入门 9.2。示例存储库 9.3。用Spring连接到MongoDB 9.3.1。使用基于Java的元数据注册Mongo实例 9.3.2。使用基于XML的元数据注册Mongo实例 9.3.3。MongoDbFactory接口 9.3.4。使用基于Java的元数据注册MongoDbFactory实例 9.3.5。使用基于XML的元数据注册MongoDbFactory实例 9.4。MongoTemplate简介 9.4.1。实例化MongoTemplate 9.4.2。WriteResultChecking策略 9.4.3。WriteConcern 9.4.4。WriteConcernResolver 9.5。保存,更新和删除文档 9.5.1。如何_id在映射图层中处理该字段 9.5.2。类型映射 9.5.3。保存和插入文件的方法 9.5.4。更新集合中的文档 9.5.5。在集合中插入文档 9.5.6。在集合中查找和插入文档 9.5.7。删除文件的方法 9.5.8。乐观锁定 9.6。查询文件 9.6.1。查询集合中的文档 9.6.2。查询文件的方法 9.6.3。查询不同的值 9.6.4。地理空间查询 9.6.5。GeoJSON支持 9.6.6。全文查询 9.6.7。排序规则 9.6.8。JSON模式 9.6.9。流利的模板API 9.7。按实例查询 9.7.1。介绍 9.7.2。用法 9.7.3。示例匹配器 9.7.4。执行一个例子 9.7.5。无类型示例 9.8。减少地图操作 9.8.1。使用示例 9.9。脚本操作 9.9.1。使用示例 9.10。集团运营 9.10.1。使用示例 9.11。聚合框架支持 9.11.1。基本概念 9.11.2。支持的聚合操作 9.11.3。投影表达式 9.11.4。分面分类 9.12。用自定义转换器覆盖默认映射 9.12.1。使用已注册的Spring Converter进行保存 9.12.2。使用Spring转换器读取 9.12.3。使用MongoConverter注册Spring转换器 9.12.4。转换器消除歧义 9.13。索引和集合管理 9.13.1。创建索引的方法 9.13.2。访问索引信息 9.13.3。使用集合的方法 9.14。执行命令 9.14.1。执行命令的方法 9.15。生命周期事件 9.16。例外翻译 9.17。执行回调 9.18。GridFS支持 9.19。更改流 9.19.1。使用MessageListener更改流 9.19.2。更改流 - 无效 10.反应性的MongoDB支持 10.1。入门 10.2。使用Spring和Reactive Streams Driver连接到MongoDB 10.2.1。使用基于Java的元数据注册MongoClient实例 10.2.2。ReactiveMongoDatabaseFactory接口 10.2.3。使用基于
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值