5

文件上传

controller层

从controller层开始,接受前台传的数据。

1.前台调用后台接口:

 /**
   * 上传文件
   * @param file 文件
   */
  upload(file: File): Observable<HttpEvent<Attachment>> {
    const formData: FormData = new FormData();
    formData.append('file', file);
    return this.httpClient.post<Attachment>(`${this.url}/upload`,
      formData, {reportProgress: true, observe: 'events'});
  }

2.controller层接收数据:

/**
   * 上传文件
   *
   * @param multipartFile 附件
   * @return 上传附件结果
   */
  @PostMapping("upload")
  @JsonView(UploadJsonView.class)
  public Attachment upload(@RequestParam("file") MultipartFile multipartFile) throws Exception {
    return this.attachmentService.upload(multipartFile);
  }

可以看到,后台接受文件是以 MultipartFile 类型来接收.

MultipartFile其实是一个接口,其中定义提供了上传文件的各方面信息

MultipartFile 方法说明:

返回值类型方法说明
byte[] getBytes()将文件内容转化成一个byte[] 返回
String getContentType()返回文件的内容类型
InputstreamgetInputStream() 返回InputStream读取文件的内容
String getOriginalFilename()返回文件的名称
Long getSize()返回文件大小 以字节为单位
BooleanisEmpty() 判断是否为空,或者上传的文件是否有内容
Void transferTo(File dest)用来把 MultipartFile 转换换成 File

有了上面提供的函数,我们就可以很轻松的操作它。

3.service层

3.1: 保存文件

既然是文件上传,那么我们肯定要在后台中保存文件。

那么,我们是选择保存在数据库中还是后台的文件夹中?

答案推荐是:保存在文件夹中,或者说是保存在硬盘上。

数据库直接存储文件的话,比如一些小的图片,如几十K的头像图片等,是可以直接转换成二进制,存放到数据库中的。但是一般项目开发都不会在数据库中存文件,否则访问文件可能会对数据库造成很高的负载。



所以我们可以,在创建一个attachment文件夹, 将文件保存在下面。
image.png

那么, 如何将MultipartFile类型的文件,保存到文件夹中呢

我们可以利用 Files提供的静态的 copy 方法, 将文件复制到目标路径中。

举个例子, 可以如下:

Path saveFilePath = Paths.get("/attachment" + this.getYearMonthDay());

String saveName =  multipartFile.getOriginalFilename();

logger.debug("将文件移动至储存文件的路径下");
Files.copy(multipartFile.getInputStream(), saveFilePath.resolve(saveName),
              StandardCopyOption.REPLACE_EXISTING);

copy 函数用了Java的输入输出流类型,将文件复制到目标路径。

3.2 保存实体数据

我们还需要做两件事:

  • 保存文件路径。
  • 对文件进行MD5加密

为了在保存文件之后,获取这个文件,这就需要我们保存这个路径。通过路径获取文件。

同时,我们还需要进行对文件进行MD5加密, 原因有二:

确保完整性: 我们将文件生成了MD5摘要,传输文件和MD5码给接收端,接收端接收文件后可以对文件生成MD5码然后与接收到的MD5码对比,校验确保文件是完整的而且中途没有被修改.

确保有效性: 发送文件过去后,可以要求前台返回文件的MD5码,你可以将收到的MD5码和自己文件的MD5码校验,确保通信为有效;还有就是可以将文件存储起来,MD5码也存储在数据库,以便复查的时候确保文件是传输成功了的。

所以,现在现在我们需要创建一个实体,保存文件路径,以及md5加密值等属性。

@Entity
public class MyFile {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  protected Long id;

  private String sha1;

  private String md5;

  /**
   * 文件储存路径
   */
  private String path;

  /**
   * 文件储存名称
   */
  private String name;

  /**
   * 文件MIME,对应文件类型
   */
  private String mime;

保存实体:

Path saveFilePath = Paths.get(CONFIG_PATH + this.getYearMonthDay());
String sha1ToMultipartFile = CommonService.encrypt(multipartFile, "SHA-1");
String md5ToMultipartFile = CommonService.encrypt(multipartFile, "MD5");

MyFile file = new MyFile();
file.setName(saveName);
file.setMime(multipartFile.getContentType());
file.setPath(saveFilePath.toString());
file.setSha1(sha1ToMultipartFile);
file.setMd5(md5ToMultipartFile);
oldFile = this.myFileRepository.save(file);

保存好路径之后,我们以后就可以查询该实体,获取路径之后,进行下载文件。

image.png

这里讲了大概的思路,相关函数代码篇幅过多。

文件下载

controller层:

/**
   * 下载
   *
   * @param id       id
   * @param md5      md5
   * @param filename 保存的文件名
   * @param response 响应
   * @throws UnsupportedEncodingException 传入的文件名转码失败时异常
   */
  @GetMapping({"download/{id}/{md5}",})
1  public void download(@PathVariable Long id,
2                       @PathVariable String md5,
3                       @RequestParam(required = false) String filename,
4                       HttpServletResponse response)
5          throws UnsupportedEncodingException {
6    if (filename != null) {
7      response.setHeader("Content-disposition", "attachment; filename=" +
8              URLEncoder.encode(filename, "UTF-8"));
9    }
10    this.attachmentService.download(id, md5, response);
  }

下载文件需要3个参数, 附件id, md5值,用来比对文件。 filename这里可以自己设置文件的名称。

当前台给Web服务器发送一个http请求,web服务器就会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。

第四行,这里我们通过获取HttpServletResponse对象,设置返回的响应。

第七行的Content-disposition,Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。

可以控制用户请求所得的内容,文件直接在浏览器上显示或者在访问时弹出文件下载对话框。

简单来说就是,我们设置了响应类型为,弹出文件下载。

Service

1.判断md5值是否相同

如果下载的文件和数据库中的md5值相同, 再进行文件下载。

 @Override
  public void download(Long id, String md5, HttpServletResponse response) {
    Attachment attachment = this.getById(id);
    if (!attachment.getFile().getMd5().equals(md5)) {
      throw new EntityNotFoundException();
    }
    this.myFileService.download(attachment.getName(), attachment.getFile(), response);
  }

2.文件下载

我们进行如下的步骤

1.根据文件路径获取硬盘文件

Path path = Paths.get(myFile.getPath())
        .resolve(myFile.getName());
    java.io.File file = path.toFile();

2.设置响应的类型,长度

response.setHeader("Content-Type", myFile.getMime());

logger.debug("输出文件长度");
response.setContentLength((int) file.length());

3.用输入输出流,将文件copy写入响应的输出流中。

org.apache.commons.io.IOUtils.copy(inputStream, response.getOutputStream());

4.用flushBuffer,提交该响应,返回前台。

response.flushBuffer();

flush包含两个步骤:先将缓冲区内容发送至客户端,然后将缓冲区清空)。这就标志着该次响应已经committed(提交)。对于当前页面中已经committed(提交)的response,就不能再使用这个response向缓冲区写任何东西

public void download(String filename, MyFile myFile, HttpServletResponse response) {
    Path path = Paths.get(myFile.getPath())
        .resolve(myFile.getName());
    java.io.File file = path.toFile();
    logger.debug("输出文件类型");
    FileInputStream inputStream;
    try {
      inputStream = new FileInputStream(file);
    } catch (FileNotFoundException e) {
      logger.error("读取文件出错" + myFile.getId() + file.getAbsolutePath());
      e.printStackTrace();
      throw new RuntimeException("读取文件发生错误");
    }

    response.setHeader("Content-Type", myFile.getMime());

    logger.debug("输出文件长度");
    response.setContentLength((int) file.length());

    logger.debug("写入数据流");
    try {
      org.apache.commons.io.IOUtils.copy(inputStream, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException e) {
      logger.error("下发数据时发生了错误");
      e.printStackTrace();
      throw new RuntimeException("下发数据时发生了错误");
    }
  }

至此,文件就被复制到响应的数据流中, 浏览器就会下载文件。


weiweiyi
1k 声望123 粉丝