一、前端设计

1. 页面设计

前端页面这里我选择了 Vue Antd 提供的上传组件实现,页面效果如下:

当用户未选择文件之前 上传 按钮是状态是不可点击,选择文件之后点击上传则将文件传至后端进行保存,下载则通过文件名进行匹配。

页面实现示例代码如下:

  <div :style="{margin: '10px 20px'}">
    <!-- File upload -->
    <a-upload :file-list="fileList" :remove="handleRemove" :before-upload="beforeUpload">
      <a-button>
        <a-icon type="upload"/>
        选择文件
      </a-button>
      <a-button
        type="primary"
        :disabled="fileList.length === 0"
        :loading="uploading"
        @click="handleUpload"
      > {{ uploading ? '上传中' : '上传' }}
      </a-button>
    </a-upload>
    <!-- File download -->
    <a-row :style="{marginTop: '20px'}">
      <a-col :span="6">
        <a-input
          v-model="fileName"
          placeholder="请输入文件名"
        />
      </a-col>
      <a-col :span="1"/>
      <a-col :span="2">
        <a-button
          type="primary"
          @click="download('file')"
        >下载</a-button>
      </a-col>
      <a-col :span="2">
        <a-button
          type="primary"
          @click="download('export')"
        >导出</a-button>
      </a-col>
    </a-row>
  </div>
</template>

2. 方法设计

上述页面中定义的数据与方法代码如下,其中 files.js 为通过 axios 定义的请求接口,在后续的后端接口设计中将会提供。

其中较为核心的部分即为 handleUpload() 与 download() 方法,分别对应文件的上传与下载,这里详细讲解一下 handleUpload() 的操作。

通常文件上传使用 Content-Type 为 multipart/form-data,因此这里使用 FormData() 对象将用户上传的对象传至后端,通过 forEach 函数将文件列表依次由 append() 进行赋值,后端将通过数组进行接收。

handleUpload() {
    const {fileList} = this;
    this.uploading = true;
    // 创建表单对象
    const data = new FormData()
    fileList.forEach(file => {
        // 表单数据赋值
        data.append("files", file)
    })
    // 调用接口上传
    uploadFile(data).then(res => {
        if (res) {
            this.fileList = [];
            this.$message.success('上传成功');
            this.uploading = false;
        } else {
            this.$message.error('上传失败');
            this.uploading = false;
        }
    })
}

完成的方法示例代码如下:

<script>
import {uploadFile, downloadFile, downloadExcel} from '@/api/files.js';

export default {
  data() {
    return {
      fileList: [],
      uploading: false,
      fileName: ''
    }
  },
  methods: {
    handleRemove(file) {
      const index = this.fileList.indexOf(file);
      const newFileList = this.fileList.slice();
      newFileList.splice(index, 1);
      this.fileList = newFileList;
    },
    beforeUpload(file) {
      this.fileList = [...this.fileList, file];
      return false;
    },
    handleUpload() {
      const {fileList} = this;
      this.uploading = true;
      const data = new FormData()
      fileList.forEach(file => {
        data.append("files", file)
      })
      uploadFile(data).then(res => {
        if (res) {
          this.fileList = [];
          this.$message.success('上传成功');
          this.uploading = false;
        } else {
          this.$message.error('上传失败');
          this.uploading = false;
        }
      })
    },
    download(data) {
      switch (data) {
        case 'file':
          const file = this.fileName
          if (file !== null && file !== '') {
            this.$message.info("下载文件")
            downloadFile(file)
          } else {
            this.$message.error("请输入文件名")
          }
          break;
        case 'export':
          downloadExcel()
          break;
      }
    }
  }
}
</script>

二、文件上传

1. 文件接收

MultipartFile 为 Spring 提供的文件类用于文件的上传等操作,这里定义了一个数组对象接收前端的传输的文件集合。

@RestController
@RequestMapping("/api/file")
public class FileController {

    @Autowired
    private FileService fileService;

    @PostMapping("upload")
    public void upload(@RequestParam("files") MultipartFile[] files) {
        for (MultipartFile file : files) {
            // 保存文件
            fileService.saveFile(file);
        }
    }
}

2. 文件转存

后端在选择转存文件时存在两种方案,第一种即转存至当前运行服务器上,第二种即转存至第三方对象存储服务中(如 OSS、MinIO 等),这里以本地转为例。

上一步中的 saveFile() 方法代码如下,通过 Files.copy() 将接收的文件进行转存。

public class FileServiceImpl implements FileService {

    private final Path path = Paths.get("E:\\Temporary\\");

    @Override
    public void saveFile(MultipartFile file) {
        if (file.isEmpty()) {
            return;
        }
        String fileName = file.getOriginalFilename();
        if (Objects.isNull(fileName) || fileName.isEmpty()) {
            fileName = UUID.randomUUID().toString();
        }
        try {
            Files.copy(file.getInputStream(), path.resolve(fileName));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

3. 接口对接

为了方便接口调用这里通过 Axios 对接口进行简单封装,注意需要在请求头中指定 Content-Type 为 multipart/form-data 表示表单文件上传。

import request from './util/axios';

const prefix = "/api/file"

export function uploadFile(params) {
  return request({
    method: 'post',
    url: `${prefix}/upload`,
    data: params,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}

三、文件下载

1. 文件读取

通过传入的文件名从目标路径读取文件,并将文件转为字节数组写入相应体返回前端以供下载。

完整的实现代码示例如下:

@RestController
@RequestMapping("/api/file")
public class FileController {
    
    @Autowired
    private FileService fileService;

    @GetMapping("download")
    public void download(@RequestParam(value = "fileName") String fileName,
                         HttpServletResponse response) {
        fileService.download(fileName, response);
    }
}

@Service
public class FileServiceImpl implements FileService {
    
    private final Path path = Paths.get("E:\\Temporary");
    
    public void download(String fileName, HttpServletResponse response) {
        try {
            // 读取文件
            File file = new File(path + File.separator + fileName);
            if (!file.exists()) {
                throw new IllegalArgumentException("文件不存在");
            }
            // 读取类型
            String type = Files.probeContentType(file.toPath());
            response.setContentType(type);
            response.setHeader("filename", URLEncoder.encode(fileName, "UTF-8"));
            // 写入文件
            response.getOutputStream()
                    // 写入数据
                    .write(inputStreamToByte(Files.newInputStream(file.toPath())));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * InputStream 转 byte 数组
     */
    private static byte[] inputStreamToByte(InputStream in) {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            int ch;
            byte[] buff = new byte[100];
            while ((ch = in.read(buff, 0, 100)) > 0) {
                bos.write(buff, 0, ch);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2. 文件导出

在下载文件时除了直接读取已有文件之外通常还有一种情况即从数据库表中导出数据,最常用的第三方工具即 easyexcel,在项目中导入下述依赖。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>

easyexcel 工具可以编辑的将 Java 中的集合列表等数据结构转为 Excel 文件以供下载,其基本 API 参考下表。

方法 作用
head() 防止。
sheet() 判断。
registerWriteHandler() 判断。
doWrite() 委托。

完整的 Excel 文件导出示例代码如下:

@GetMapping("export")
public void downloadExcel(HttpServletResponse response) throws IOException {
    // 设置请求头
    response.setContentType("application/vnd.ms-excel");
    response.setHeader("filename", URLEncoder.encode("user-list", "UTF-8"));
    response.setCharacterEncoding("utf-8");
    // 查询数据列表
    List<SysUser> userList = sysUserService.queryAll();
    String[] headers = {"编号", "用户名", "简介", "性别", "生日", "电话"};
    List<List<String>> collect = Stream.of(headers)
            .map(Arrays::asList)
            .collect(Collectors.toList());
    // 写入请求
    EasyExcel.write(response.getOutputStream())
            // Excel 头
            .head(collect)
            .sheet("Info")
            // 宽度自适应
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
            // 写入内容
            .doWrite(userList);
}

3. 接口设计

与文件上传同理这里通过 Axios 对文件下载接口进行封装实现更便捷调用,代码内容如下:

import request from './util/axios';

const prefix = "/api/file"

export const downloadFile = params => request({
  method: 'get',
  url: `${prefix}/download`,
  params: params,
  responseType: 'blob',
  headers: {
    'Access-Control-Allow-origin': '*',
    'Content-Type': 'charset=UTF-8'
  }
}).then((res) => {
  if (res && res.status === 200) {
    // 读取文件名
    let fileName = '' + res.headers.filename
    fileName = decodeURI(fileName)
    // 读取文件数据
    const blob = new Blob([res.data])
    let brower = ''
    if (navigator.userAgent.indexOf('Edge') > -1) {
      brower = 'Edge'
    }
    if ('download' in document.createElement('a')) {
      if (brower === 'Edge') {
        navigator.msSaveBlob(blob, fileName)
        return
      }
      const url = window.URL.createObjectURL(res.data)
      const link = document.createElement('a')
      link.style.display = 'none'
      link.href = url
      link.setAttribute('download', fileName)
      if (!document.getElementById(fileName)) {
        document.body.appendChild(link)
      }
      link.click()
      URL.revokeObjectURL(link.herf)
      document.body.removeChild(link)
    } else {
      // IE10+下载
      navigator.msSaveBlob(blob, fileName)
    }
  }
}).catch(error => {
  console.log(error)
})

export const downloadExcel = params => request({
  method: 'get',
  url: `${prefix}/export`,
  params: params,
  responseType: 'blob',
  headers: {
    'Access-Control-Allow-origin': '*',
    'Content-Type': 'charset=UTF-8'
  }
}).then((res) => {
  if (res && res.status === 200) {
    // 设置文件后缀
    let fileName = '' + res.headers.filename + '.xlsx'
    fileName = decodeURI(fileName)
    // 读取文件数据
    const blob = new Blob([res.data])
    let brower = ''
    if (navigator.userAgent.indexOf('Edge') > -1) {
      brower = 'Edge'
    }
    if ('download' in document.createElement('a')) {
      if (brower === 'Edge') {
        navigator.msSaveBlob(blob, fileName)
        return
      }
      const url = window.URL.createObjectURL(res.data)
      const link = document.createElement('a')
      link.style.display = 'none'
      link.href = url
      link.setAttribute('download', fileName)
      if (!document.getElementById(fileName)) {
        document.body.appendChild(link)
      }
      link.click()
      URL.revokeObjectURL(link.herf)
      document.body.removeChild(link)
    } else {
      // IE10+下载
      navigator.msSaveBlob(blob, fileName)
    }
  }
}).catch(error => {
  console.log(error)
})
(adsbygoogle = window.adsbygoogle || []).push({});