参考资料
⏹前端接受后端文件流并下载的几种方法
⏹ajax 请求二进制流 图片 文件 XMLHttpRequest 请求并处理二进制流数据 之最佳实践
⏹ajax请求二进制流进行处理(ajax异步下载文件)
前端
JQuery的ajax方法并不支持返回流的方式,因此如果要想将文件流对象返回给前端,要使用原生Ajax
2022/07/09 勘误
支持流的方式返回前端,详情见方式二
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<button id="btn">下载本地的文本button>
body>
<script th:src="@{js/jquery.min.js}">script>
<script>
// 点击按钮下载,将本地的文件以流的形式返回前端,然后进行下载
$("#btn").on("click", function() {
// 构造XMLHttpRequest对象
var xmlRequest = new XMLHttpRequest();
// 发送get请求
xmlRequest.open("GET", "/Test/fileDownLoad", true);
// 设置响应类型
xmlRequest.responseType = "blob";
// 发送请求
xmlRequest.send([]);
// 请求获得响应之后,触发下面的回调函数
xmlRequest.onload = function(oEvent) {
// 当时满足下面的状态码的时候,视为下载成功
if ((xmlRequest.status >= 200 && xmlRequest.status < 300) || xmlRequest.status === 304) {
// 从xmlRequest对象中获取响应的内容
var content = xmlRequest.response;
/*
从xmlRequest的响应头中获取文件名字符串
因为我们将文件以流的形式返回前端,返回的文件没有文件名
因此在后端处理的时候,我们将文件名放到响应头中
然后在前端从响应头中获取文件名
*/
var dispositionStr = xmlRequest.getResponseHeader('content-disposition');
if (dispositionStr == null || dispositionStr === "") {
alert("下载失败!");
return;
}
// 获取文件名
let dispositionArr = dispositionStr.split(";");
// 我们的文件名可能含有汉字,因此在后端进行了UTF-8编码处理,此处进行解码
let fileName = decodeURIComponent(dispositionArr[1]);
// 利用response的响应内容构造一个Blob对象(通过Blob对象进行下载)
var blob = new Blob([content]);
// 创建一个隐藏的用来下载文件的a标签
var elink = document.createElement('a');
elink.download = fileName;
elink.style.display = 'none';
/*
将blob文件对象转换为内存中对象,并将生成的对象赋给隐藏的a标签
bolb对象会暂时存储在客户端的内存中,
使用URL.createObjectURL()方法可以创建指向内存文件对象的临时url
使用createObjectURL可以节省性能并更快速,只不过需要在不使用的情况下手动释放内存
FileReader.readAsDataURL返回文件的base64字符串,比blob url消耗更多内存,但是在不用的时候会自动从内存中清除(通过垃圾回收机制)
*/
const src = URL.createObjectURL(blob);
elink.href = src;
document.body.appendChild(elink);
// 模拟点击下载事件,进行下载
elink.click();
// 点击之后,移除我们定义的隐藏a标签
document.body.removeChild(elink);
// 移除文件对象
URL.revokeObjectURL(src)
}
}
});
script>
html>
后端
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@GetMapping("/fileDownLoad")
public void fileDownload(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 保存在本地磁盘中的文件
File file = new File("C:\\Users\\jiafeitian\\Desktop\\如果让你来设计网络.mhtml");
BufferedInputStream bis = null;
FileInputStream fileInputStream = null;
try {
response.setHeader("content-type", "application/octet-stream");
/*
我们将文件以流的方式返回前端,但是流无法保存文件名称,因此我们将问文件名称放响应头中返回到前端,前端就可以获取到下载的文件名称了
在响应头中设置文件名,通过URLEncoder.encode()进行文件编码防止文件名乱码
*/
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
// 设置响应类型
response.setContentType("application/x-download");
// 读取本地的文件
fileInputStream = new FileInputStream(file);
// 根据输入流构造一个输入缓冲流对象
bis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[1024];
int i = bis.read(buffer);
// 根据response对象构造一个输出流对象,然后将输入流的对象写入输出流
OutputStream os = response.getOutputStream();
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bis != null) {
bis.close();
}
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
⭕后台-controller
添加
@ResponseBody注解
,防止 java.lang.IllegalStateException: getOutputStream() has already been called for this response 异常.如果使用@Autowired的方式注入HttpServletResponse
对象,但是不添加@ResponseBody注解话,就会出现该异常.
@Controller
@RequestMapping("/test2")
public class Test2Controller {
@Autowired
private Test2Service service;
@GetMapping("/init")
public ModelAndView init() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test2");
return modelAndView;
}
@PostMapping("/fileDownload")
@ResponseBody
public void fileDownload(@RequestBody Test2Form form) throws Exception {
service.fileDownload(form);
}
}
⭕后台-service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.net.URLEncoder;
import java.text.MessageFormat;
@Service
public class Test2Service {
@Autowired
private ResourceLoader resourceLoader;
@Autowired
private HttpServletResponse response;
public void fileDownload(Test2Form form) throws Exception {
String fileId = form.getFileId();
String fileName = form.getFileName();
// 格式化拼接资源的相对路径
String filePath = MessageFormat.format("classpath:temp/{0}/{1}", fileId, fileName);
// 使用ResourceLoader获取项目中的资源,防止项目打包之后找不到资源
Resource resource = resourceLoader.getResource(filePath);
if (!resource.exists()) {
// 抛出异常,前台Ajax在error方法中对异常进行处理,获取响应头中的异常信息
response.setHeader("errorInfo", URLEncoder.encode(fileName + "不存在!", "UTF-8"));
throw new RuntimeException();
}
// 获取资源,指定相应信息
File file = resource.getFile();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename*=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
response.setContentLength((int) file.length());
// 要下载的文件不大的话,直接使用工具类将文件拷贝到响应流中即可
FileCopyUtils.copy(new FileInputStream(file), response.getOutputStream());
}
}
⭕前台
㊙关键在于dataType和xhrFields的属性指定上,指定对属性才能返回blob文件流
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<button id="btn">点击下载文件button>
body>
<script src="/js/public/jquery-3.6.0.min.js">script>
<script th:inline="javascript">
function doAjax_FileDownload(url, param, callback) {
$.ajax({
url: url,
type: "post",
data: JSON.stringify(param),
// 向服务器发送的数据类型
contentType: 'application/json;charset=utf-8',
// 指定服务器返回的类型,因为我们要返回文件流,所以类型为二进制数据
dataType: "binary",
// 原生 XMLHttpRequest 的属性,设置响应类型为blob,接收文件流
xhrFields: {
'responseType': 'blob'
},
success: function (result, status, xhr) {
// 可通过XMLHttpRequest对象,获取响应头
console.log(xhr);
// 浏览器兼容
const download_URL = (window.URL || window.webkitURL).createObjectURL(result);
// 创建a标签,模拟点击下载
const a_link = document.createElement('a');
a_link.href = download_URL;
// 利用了a标签的download属性,指定文件名称
a_link.download = param.fileName;
document.body.appendChild(a_link);
a_link.click();
setTimeout(function () {
// 移除内存中的临时文件路径和为下载而创建的a标签
URL.revokeObjectURL(download_URL);
a_link.remove();
}, 10000);
},
error: function (xhr, textStatus, errorMessage) {
// 从响应头中获取异常信息,如果包含中文的话会乱码因此 后台URLEncoder.encode() + 前台decodeURIComponent() 防止乱码
const errorInfo = decodeURIComponent(xhr.getResponseHeader("errorInfo"));
// 对错误信息进行展示
alert(errorInfo);
}
});
}
$("#btn").click(() => {
const url = "http://localhost:8080/test2/fileDownload";
const fileInfo = {
fileName: '测试文件.text',
fileId: 'A110120119'
};
doAjax_FileDownload(url, fileInfo, function () {});
});
script>
html>
重要代码为
await response.blob()
详情可参考这篇博客
https://blog.csdn.net/feyehong/article/details/124974601