后端controller
@GetMapping("/export")
public AjaxResult export(ProcSpecif procSpecif) {
List<ProcSpecif> procSpecifsList = procSpecifMapper.selectList(null);
// 用于存放转换后数据的列表,每个元素是一个Map,代表Excel的一行
List<Map<String, Object>> mapList = new ArrayList<>();
// 遍历查询结果,将数据转换为Map形式
for (int i = 0; i < procSpecifsList.size(); i++) {
ProcSpecif row = procSpecifsList.get(i);
Map<String, Object> map = new LinkedHashMap<>();//LinkedHashMap保持插入顺序
// getSpecification和getTime是ProcSpecif类的方法
map.put("规格", row.getSpecification());
map.put("时间", DateUtil.formatChineseDate(row.getTime(), false, false));
mapList.add(map);}
// 生成导出的文件名,包含UUID和时间戳
String filename = IdUtil.fastSimpleUUID() + "_规程" + DateUtils.getDate() + ".xlsx";
// 创建ExcelWriter对象用于写入Excel
ExcelWriter exportWriter = ExcelUtil.getWriter();
// 将数据写入Excel,并设置表头是否自动生成(这里为true,表示自动从数据生成表头)
exportWriter.write(mapList, true);
try {
// getAbsoluteFile用于获取文件的绝对路径
OutputStream out = new FileOutputStream(getAbsoluteFile(ProjectConfig.getDownloadPath(), filename));
// 将ExcelWriter的内容写入到OutputStream中,并关闭输出流
exportWriter.flush(out, true);
} catch (IOException e) {
e.printStackTrace();
throw new CustomException("导出失败");
} finally {
exportWriter.close();
}
return AjaxResult.success(filename);
}
里面比较关键的部分代码:
ExcelWriter exportWriter = ExcelUtil.getWriter();
exportWriter.write(mapList, true);
try {
OutputStream out = new FileOutputStream(getAbsoluteFile(ProjectConfig.getDownloadPath(), filename));
exportWriter.flush(out, true);
}
可以理解为,OutputStream那一步已经生成了一个空的excel文件,然后exportWriter.flush时就将exportWriter中的内容去覆盖这个空文件的内容。也就是说,你的服务器上的某个指定文件夹里面已经有了这个文件。
以上为便于理解的说法,实际上是在exportWriter.flush才生成excel文件。
前端:
由于后端传递的res中包含了文件的路径名,直接调用this.download即可,这里的res.msg实际上就是filename。
this.download(res.msg)
download函数的实现:
export function download(fileName) {
window.location.href = baseURL + "/common/download?fileName=" + encodeURI(fileName) + "&delete=" + false;
}
这句话实际上是表示向后端的 /common/download 这个路由发送请求。
进入该路由:
@GetMapping("common/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response) {
try {
if (!FileUtils.checkAllowDownload(fileName)) {
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = fileName.substring(fileName.indexOf("_") + 1);
String filePath = ProjectConfig.getDownloadPath() + fileName;
System.out.println("OOOOOOO");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete) {
FileUtils.deleteFile(filePath);
}
} catch (Exception e) {
log.error("下载文件失败", e);
}
}
- 安全检查:首先,通过
FileUtils.checkAllowDownload(fileName)
方法检查文件名是否合法,如果不合法,则抛出异常,并提示用户文件名称非法,不允许下载。 - 文件名处理:通过
fileName.substring(fileName.indexOf("_") + 1)
获取真正的文件名(假设文件名中包含一个下划线_
,并且真正的文件名位于_
之后)。 - 文件路径构建:使用
ProjectConfig.getDownloadPath()
获取下载文件的根目录,并与文件名拼接成完整的文件路径。 - 输出响应:
-
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);设置响应内容类型为
application/octet-stream
,这是一种通用的二进制流MIME类型,用于指示文件下载。 - 通过
FileUtils.setAttachmentResponseHeader(response, realFileName)
设置响应头,以便浏览器将响应视为文件下载,并指定下载文件的名称。 - 使用
FileUtils.writeBytes(filePath, response.getOutputStream())
将文件内容写入响应的输出流中,这样浏览器就可以接收并保存文件了。
-
- 文件删除:如果
delete
参数为true
,则在文件下载后删除文件。这是通过FileUtils.deleteFile(filePath)
方法实现的。