一. 文件存储到MongoDB中,借助GridFS

1.介绍

MongoDB是一种非关系型数据库(NoSql),很好的实现了面向对象的思想(OO思想),在Mongo DB中 每一条记录都是一个Document对象。Mongo DB最大的优势在于所有的数据持久操作都无需开发人员手动编写SQL语句,直接调用方法就可以轻松的实现CRUD操作。
GridFS是MongoDB的一种存储机制,用来存储大型二进制文件,最适合于不常改变但是经常需要连续访问的大文件。

2.java代码

controller层:

/**
     * 上传单份资料
     *
     * @param file
     * @return
     * @throws IOException
     * @throws ServletException
     */
    @ApiOperation(value = "上传单份资料", notes = "上传单份资料", produces = "application/json")
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public ReturnInfo upload(@RequestParam("subjectId") Integer subjectId, @RequestParam("file") MultipartFile file) throws IOException, ServletException {
        try {
            String fName = file.getOriginalFilename(); // 获得提交的文件名
            InputStream ins = file.getInputStream(); // 获取文件输入流
            String contentType = file.getContentType(); // 获取文件类型
            Long fileSize = file.getSize(); // 获取文件大小
			String fSize="";
			if(fileSize<1024*1024){
				fSize=String.valueOf(Math.ceil(fileSize/1024))+"K";
			}else{
				fSize=String.valueOf(Math.ceil(fileSize/(1024*1024)))+"M";
			}
            String[] arr = fName.split("\\.");
			int len=arr.length;
            String fileName ="";
            for(int i=0;i<len-1;i++){
            	if(i==len-2){
					fileName=fileName+arr[i];
				}
            	else{
            		fileName=fileName+arr[i]+".";
				}
			}  //最后一个.之前的为文件名称,
            String fileSuffix = arr[len-1];  //最后一个.之后的为文件类型  例如file-v1.0.1.pdf这种情况
            ObjectId objectId = gridFsTemplate.store(ins, fName, contentType); // 将文件存储到mongodb中
            logger.info("保存成功,objectId:" + objectId + "fileName:" + fileName + "fileSuffix:" + fileSuffix + "fileSize:"
                    + fSize);
            materialService.addMaterial(subjectId, fileName, fileSuffix, fSize,
                            objectId.toString());
            return new ReturnInfo("1", "上传资料成功", null, 0);
        } catch (Exception e) {
            logger.error("上传资料异常", e);
            return new ReturnInfo("0", "上传资料失败", null, 0);
        }
    }
    
	/**
	 * 下载资料
	 *
	 * @param mId
	 * @param request
	 * @param response
	 * @throws IOException
	 */

	@ApiOperation(value = "下载资料", notes = "下载资料", produces = "application/json")
	@ApiImplicitParams(value = {
			@ApiImplicitParam(name = "mId", value = "资料id", required = true, paramType = "path", dataType = "int") })
	@RequestMapping(value = "/download/{mId}", method = RequestMethod.GET)
	public ReturnInfo download(@PathVariable("mId") Integer mId, HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		try {
			// 由mId获取pid
			Material material = materialService.getMaterialByMId(mId);
			String pId = material.getPid();

			Query query = Query.query(Criteria.where("_id").is(pId));
			// 查询单个文件
			GridFSFile gridFSFile = gridFsTemplate.findOne(query);
			if (gridFSFile == null) {
				return new ReturnInfo("0", "下载文件失败", null, 0);
			}
			String fileName = gridFSFile.getFilename().replace(",", "");
			String contentType = gridFSFile.getMetadata().get("_contentType").toString();
			logger.info("fileName:" + fileName);

			// 通知浏览器进行文件下载
			response.setContentType(contentType);
			response.setHeader("Content-Disposition",
					"attachment;filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
			GridFSBucket gridFSBucket = GridFSBuckets.create(mongoDbFactory.getDb());
			GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
			GridFsResource resource = new GridFsResource(gridFSFile, gridFSDownloadStream);
			OutputStream outputStream = response.getOutputStream();
			InputStream inputStream = resource.getInputStream();
			IOUtils.copy(inputStream, outputStream);
			outputStream.flush();
			outputStream.close();
			inputStream.close();
			return new ReturnInfo("1", "下载文件成功", null, 0);
		} catch (Exception e) {
			logger.error("下载文件异常", e);
			return new ReturnInfo("0", "下载文件失败", null, 0);
		}
	}
	
	/**
	 * 根据资料id删除资料信息和对应mongodb中文件
	 * 
	 * @param mId
	 * @return
	 */
	@ApiOperation(value = "根据资料id删除资料信息和对应文件", notes = "根据资料id删除资料信息和对应文件", produces = "application/json")
	@ApiImplicitParams(value = {
			@ApiImplicitParam(name = "mId", value = "资料id", required = true, paramType = "path", dataType = "int") })
	@RequestMapping(value = "/delete/{mId}", method = RequestMethod.DELETE)
	public ReturnInfo deleteMaterialByMId(@PathVariable("mId") Integer mId) {
		try {
			// 由mId获取pid
			Material material = materialService.getMaterialByMId(mId);
			String pId = material.getPid();
			if(pId==null){
				return new ReturnInfo("0", "不存在该文件", null, 0);
			}
			Query query = Query.query(Criteria.where("_id").is(pId));
			gridFsTemplate.delete(query);      //删除mongodb中文件

			materialService.deleteMaterialByMId(mId);   //删除数据库中资料信息
			return new ReturnInfo("1", "删除资料信息和对应文件成功", null,0);
		} catch (Exception e) {
			logger.error("删除资料信息和对应文件异常", e);
			return new ReturnInfo("0", "删除资料信息和对应文件失败", null, 0);
		}
	}
	
	/**
	 * 修改资料名称(mongodb中也要修改)
	 * 
	 * @param mId
	 * @param fileName
	 * @return
	 */
    @ApiOperation(value = "修改资料名称", notes = "修改资料名称", produces = "application/json")
    @ApiImplicitParams(value = {
	    @ApiImplicitParam(name = "mId", value = "资料id", required = true, paramType = "path", dataType = "int") ,
	    @ApiImplicitParam(name = "fileName", value = "资料新名称", required = true, paramType = "body", dataType = "object")})
    @RequestMapping(value = "/rename/{mId}", method = RequestMethod.PUT)
    public ReturnInfo updateMaterialFileName(@PathVariable("mId") Integer mId, @RequestBody Object fileName) {
        try {
            // 由mId获取pid
            Material material = materialService.getMaterialByMId(mId);
            String pId = material.getPid();
            if(pId==null){
                return new ReturnInfo("0", "不存在该文件", null, 0);
            }
            Query query = Query.query(Criteria.where("_id").is(pId));
            GridFSFile gridFSFile = gridFsTemplate.findOne(query);
            String contentType = gridFSFile.getMetadata().get("_contentType").toString();

            GridFSBucket gridFSBucket = GridFSBuckets.create(mongoDbFactory.getDb());
            GridFSDownloadStream gridFSDownloadStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
            GridFsResource resource = new GridFsResource(gridFSFile, gridFSDownloadStream);
            InputStream ins= resource.getInputStream();

            String fName=fileName+"."+material.getFileSuffix();
            ObjectId objectId = gridFsTemplate.store(ins, fName, contentType);   //mongodb保存新文件

            material.setFileName(fileName.toString());
            material.setPid(objectId.toString());
            materialService.updateMaterialByMId(material);  //更新资料信息
            gridFsTemplate.delete(query);      //删除mongodb旧文件
            return new ReturnInfo("1", "修改资料名称成功", null,0);
        } catch (Exception e) {
            logger.error("修改资料名称异常", e);
            return new ReturnInfo("0", "修改资料名称失败", null, 0);
        }
    }

service层:

public int addMaterial(int subjectId,String fileName,String fileSuffix,String fileSize,String pid) throws Exception{
        Material material=new Material();

        material.setSubjectId(subjectId);
        material.setFileName(fileName);
        material.setFileSuffix(fileSuffix);
        material.setFileSize(fileSize);
        material.setPid(pid);
        
        return materialMapper.insert(material);  //基本信息插入到数据库中
    }

model层:

public class Material implements Serializable {

    private Integer mId;

    private Integer subjectId;

    private String fileSize;

    private String fileName;

    private String fileSuffix;

    private String pid;  // 文件id
}

3.js代码 前端实现

上传功能使用upload组件:

import React, { PureComponent } from 'react'
import { Upload, Button, message, Modal, Form } from 'antd';
import { connect } from 'dva';
import Cookies from 'js-cookie';

const { Dragger } = Upload;

class UploadModal extends PureComponent {

  constructor(props) {
    super(props)
    this.state = {
      fileLists:undefined,
    }
  };

  handleOk = () => {
    console.log('handlesave');
    const { fileLists } = this.state;

    // 上传
    for (const item of fileLists) {
      const formData = new FormData()
      formData.append('subjectId', subjectId)
      formData.append('file', item)
      const url = '/upload';  //与后端上传接口一致
      fetch(url, {
        method: 'POST',
        body: formData,
      }).then(response => response.json()).then((res) => {
        console.log(res)
      });
    }
  }

  renderUpload=()=>{
    const { fileLists } = this.state;

    const props = {
      name: 'file',
      multiple: true,
      accept: "application/msword, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/pdf, image/jpeg, image/png",  // 支持上传的文件格式
      headers: {
        authorization: 'authorization-text',
      },

      onChange(info) {
        const { status } = info.file;
        if (status !== 'uploading') {
          console.log(info.file, info.file.fileList)
        }
        if (status === 'done') {
          message.success(`${info.file.name} file uploaded successfully.`);
          fileLists.push(info.file.originFileObj)

        } else if (status === 'removed') {
          fileLists.splice(fileLists.indexOf(info.file.originFileObj), 1)
        } else if (status === 'error') {
          message.error(`${info.file.name} file upload failed.`);
        }
      },
    };

    return (

      <Form encType="multipart/form-data" style={{ marginTop:20}}>
        <Dragger {...props}>
          <Button type="primary">选取文件</Button>
          <p style={{ marginTop:10}}>
            支持上传的文件格式:doc, docx, xls, xlsx, ppt, pptx, pdf, jpg, png
          </p>
        </Dragger>
      </Form>
    );
  };

  render() {
    const { visible, handleCancel } = this.props;
    const { fileLists } = this.state;
    console.log(fileLists);

    return (
      <Modal
        visible={visible}
        width={800}
        title="上传资料"
        onOk={this.handleOk}
        onCancel={() => handleCancel('uploadModalVisiable')}
      >
        {this.renderUpload()}
      </Modal>
    );
  }
}

export default UploadModal

下载功能实现:

<Button type='primary' onClick={() => this.handleDownloadMaterial(record)}>下载</Button>

handleDownloadMaterial =(record)=>{
    const { mId } = record;
    const downloadUrl = `/download/${mId}`; // 与后端接口请求地址一致
    fetch(downloadUrl, {
      method: 'get',
      credentials: 'include',
      headers: new Headers({
        'Content-Type': 'application/json',
      })
    }).then((response) => {
      response.blob().then(blob => {
        const aLink = document.createElement('a');
        document.body.appendChild(aLink);
        aLink.style.display='none';
        const objectUrl = window.URL.createObjectURL(blob);
        aLink.href = objectUrl;
        aLink.download = record.fileName;
        aLink.click();
        document.body.removeChild(aLink);
      });
    }).catch((error) => {
      console.log('文件下载失败', error);
    });
  };

二. 文件存储到数据库中,存储为blob形式

1. 存储方式

数据库中为blob,model里面用byte[]存文件字段
xml中:

<result column="context" jdbcType="BLOB" property="context" typeHandler="org.apache.ibatis.type.BlobTypeHandler"/>
<insert id="insert" 
   insert into 表名 (id,context, ...)
   values (#{id,jdbcType=BIGINT},
   #{context,jdbcType=BLOB}, ...
   )
</insert>

2. java代码

/**
     * 上传
     *
     * @param ...
     * @param file
     * @return
     * @throws Exception
     */
    @ApiOperation(value = "上传", notes = "上传", produces = "application/json")
    @RequestMapping(value = "/...", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
    public ReturnInfo upload(@RequestParam("...") ..., @RequestParam("file") MultipartFile file) throws Exception {
        String fName = file.getOriginalFilename(); // 获得提交的文件名
        String[] arr = fName.split("\\.");
        int len = arr.length;
        String fileName = "";
        for (int i = 0; i < len - 1; i++) {
            if (i == len - 2) {
                fileName = fileName + arr[i];
            } else {
                fileName = fileName + arr[i] + ".";
            }
        }
        String fileSuffix = arr[len - 1];  // 获取文件类型

        byte[] context=file.getBytes(); // 获取文件内容

        logger.info("上传文件:" + "fileName:" + fileName + "fileSuxffix:" + fileSuffix);
        
        int res = service.add();  //执行添加操作,向数据库内添加信息
        if (res == 1) {
            return new ReturnInfo("1", "上传成功", null, 0);
        } else {
            return new ReturnInfo("0", "上传失败", null, 0);
        }
    }
    
	/**
     * 根据ID获取内容(下载)
     *
     * @param id
     * @return
     */
    @ApiOperation(value = "根据ID获取内容(下载)", notes = "根据ID获取内容(下载)", produces = "application/json")
    @ApiImplicitParams(value = {
            @ApiImplicitParam(name = "id", value = "ID", required = true, paramType = "path", dataType = "Long")})
    @RequestMapping(value = "/.../{id}", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
    public void download(@PathVariable("id") Long id, HttpServletResponse response) {
        OutputStream outputStream = null;
        try {
            TextStamp textStamp = service.selectById(id);  // 根据id获取数据库内容
            byte[] context = textStamp.getContext();
            String fileName= textStamp.getStampName();
            String contentType="";
            if(textStamp.getStampType().equals("jpg")){
                contentType="image/jpeg";
            }else if(textStamp.getStampType().equals("png")){
                contentType="image/png";
            }

            response.setContentType(contentType);
            response.setHeader("Content-Disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
            outputStream = response.getOutputStream();
            outputStream.write(context);
            outputStream.flush();
        } catch (Exception e) {
            logger.error("error", e);
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                logger.error("error", e);
            }
        }
    }
    
    /**
     * 仅预览(不下载,用于页面实现图片预览)
     * 
     * @param stampId
     * @param response
     */
	@ApiOperation(value = "根据Id获取内容", notes = "根据Id获取内容", produces = "application/json")
    @ApiImplicitParams(value = {
            @ApiImplicitParam(name = "id", value = "ID", required = true, paramType = "path", dataType = "Long")})
    @RequestMapping(value = "/.../{id}", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
    public void showPhoto(@PathVariable("id") Long id, HttpServletResponse response) {
        OutputStream outputStream = null;
        try {
            byte[] context = textStampMapper.getStamp(id).getContext();
            outputStream = response.getOutputStream();
            outputStream.write(context);
            outputStream.flush();
        } catch (Exception e) {
            logger.error("error", e);
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                logger.error("error", e);
            }
        }
    }

3.js代码,前端实现

上传组件:

import React from 'react';
import router from 'umi/router';
import { Button, Card, Form, Switch, Upload, notification, message, Icon, Modal, TreeSelect } from 'antd';
import GridContent from '@/components/PageHeaderWrapper/GridContent';
import { connect } from 'dva';
import Cookies from 'js-cookie';

const FormItem = Form.Item;

@Form.create()
class StampAdd extends React.PureComponent {

  constructor(props) {
    super(props)
    this.state = {
      fileLists: [],
      loading: false,
      previewVisible: false,
      previewImage: '',
    }
  };

  back = () => {
    router.goBack();
  };

  getBase64 = (file) => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result);
      reader.onerror = error => reject(error);
    });
  }

  beforeUpload = (file) => {
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('You can only upload JPG/PNG file!');
    }
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
      message.error('Image must smaller than 2MB!');
    }
    return isJpgOrPng && isLt2M;
  }

  handleSave = () => {
    console.log('save');
    const { fileLists } = this.state;

    for (const item of fileLists) {
      const formData = new FormData()
	  formData.append(...); // 添加文件之外的字段 @RequestParam
      formData.append('file', item);
      const url = '/...';  // 后端上传接口
      fetch(url, {
        method: 'POST',
        body: formData,
      }).then(response => response.json()).then((res) => {
        console.log(res)
        const {retCode, retValue} = res;
        if(retCode && retCode === '1'){
          notification.success({
            message: `上传成功`,
            description: retValue
          })
        }else {
          notification.error({
            message: `上传失败`,
            description: retValue
          })
        }
      });
    }
  }
  
  normFile = e => {
    // console.log('Upload event:', e);
    if (Array.isArray(e)) {
      return e;
    }
    return e && e.fileList;
  };
  
  handleChange = info => {
    const { fileLists } = this.state;
    if (info.file.status === 'uploading') {
      this.setState({ loading: true });
      return;
    }
    if (info.file.status === 'done') {
      fileLists.push(info.file.originFileObj)
    }else if (info.file.status === 'removed') {
      fileLists.splice(fileLists.indexOf(info.file.originFileObj), 1)
    }
  };

  handleCancel = () => this.setState({ previewVisible: false });

  handlePreview = async file => {
    if (!file.url && !file.preview) {
      // eslint-disable-next-line no-param-reassign
      file.preview = await this.getBase64(file.originFileObj);
    }

    this.setState({
      previewImage: file.url || file.preview,
      previewVisible: true,
    });
  };

  render() {
    const formItemLayout = {
      labelCol: {
        xs: { span: 4 },
        sm: { span: 4 },
      },
      wrapperCol: {
        xs: { span: 10 },
        sm: { span: 10 },
      },
    };

    const { loading, previewVisible, previewImage} = this.state;
    const uploadButton = (
      <div>
        {{loading} ? <Icon type="plus" /> : <Icon type="loading" />}
        <div className="ant-upload-text">Upload</div>
      </div>
    );

    return (
      <GridContent>
        <div>
          <Card
            title="新建"
            bordered={false}
            extra={
              <div>
                <Button type="primary" onClick={this.back} style={{ marginLeft: 30 }}>取消</Button>
                <Button type="primary" onClick={this.handleSave} style={{ marginLeft: 30 }}>保存</Button>
              </div>}
          >
            <Form {...formItemLayout}>
              <Form.Item label="上传">
                {getFieldDecorator('file', {
                  valuePropName: 'fileList',
                  getValueFromEvent: this.normFile,
                  rules: [{ required: true, message: "请选择文件", },],
                })(
                  <div>
                    <Upload
                      name="file"
                      listType="picture-card"
                      className="file-uploader"
                      beforeUpload={this.beforeUpload}
                      onChange={this.handleChange}
                      onPreview={this.handlePreview}
                    >
                      {uploadButton}
                    </Upload>
                    <Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
                      <img alt="file" style={{ width: '100%' }} src={previewImage} />
                    </Modal>
                  </div>
                )}
              </Form.Item>
            </Form>
          </Card>
        </div>
      </GridContent>
    );
  }
}

export default StampAdd;

下载功能:

<Button onClick={()=>this.handleDownload(record)}>下载</Button>

handleDownload =(record)=>{
    const downloadUrl = `/.../${record.id}`; // 接口请求地址
    fetch(downloadUrl, {
      method: 'get',
      credentials: 'include',
      headers: new Headers({
        'Content-Type': 'application/json',
      })
    }).then((response) => {
      response.blob().then(blob => {
        const aLink = document.createElement('a');
        document.body.appendChild(aLink);
        aLink.style.display='none';
        const objectUrl = window.URL.createObjectURL(blob);
        aLink.href = objectUrl;
        aLink.download = `${record.name}.${record.type}`;
        aLink.click();
        document.body.removeChild(aLink);
      });
    }).catch((error) => {
      console.log('文件下载失败', error);
      notification.success({
        message: `文件下载失败`,
        description: error,
      })
    });
  };

图片预览:

handlePreview =(record)=>{
    const { id } = record;
    const previewUrl = `/.../${id}`; // 接口请求地址
    fetch(previewUrl, {
      method: 'get',
      credentials: 'include',
      headers: new Headers({
        'Content-Type': 'application/json',
      })
    }).then((response) => {
      response.blob().then(blob => {
        const aLink = document.createElement('a');
        document.body.appendChild(aLink);
        aLink.style.display='none';
        const objectUrl = window.URL.createObjectURL(blob);
        this.setState({
          previewImageUrl: objectUrl,
        });
      });
    }).catch((error) => {
      console.log('图片显示失败', error);
    });
  };

<img src={previewImageUrl} alt="file" style={{ width: '100%' }} />