1、问题描述
我想通过异步的方式实现下载文化,请求为post请求。一开始我打算用ajax。
$.ajax({
type:'post',
contentType:'application/json',
url:'http://xxx/downloadExcel',
data:{data:JSON.stringify(<%=oJsonResponse.JSONoutput()%>)},
}).success(function(data){
const blob = new Blob([data], {type: 'application/vnd.openxmlformats-
officedocument.spreadsheetml.sheet'});
const url1=URL.createObjectURL(blob)
const a = document.createElement('a');
a.href = url1;
a.download = '表格.xlsx';
a.click();
URL.revokeObjectURL(url1);
});
不过ajax的返回类型不支持二进制文件流(binary)!因此ajax的异步方式无法接到后端接口返回的文件流,就无法下载文件。
jQuery.ajax() | jQuery API Documentation
2、解决方法
改用dom原生的XMLHttpRequest。
XMLHttpRequest的返回类型二进制数据blob,可以接到文件流。
XMLHttpRequest.responseType - Web API 接口参考 | MDN (mozilla.org)
3、代码示例
3.1、前端代码
downloadExcel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript" src="./js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#btnDownload").click(function(){
var param={name:'zhangsan',age:'20',sex:'男'};
let xhr=new XMLHttpRequest();
xhr.responseType = "blob";
xhr.open('POST', 'http://localhost:6001/excel/downloadExcel');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(param));
xhr.onreadystatechange = function() {
console.log(xhr.response)
if (xhr.status === 200) {
var blob = xhr.response;
if(blob){
var downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = 'excel.xlsx'; // 设置下载的文件名
downloadLink.click();
}
}
}
});
});
</script>
<button class="btn" id="btnDownload" name="btnDownload">下载文件</button>
</body>
</html>
3.2、后端代码
ExcelController.java
import java.io.*;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author Wulc
* @date 2023/7/20 16:02
* @description
*/
@RestController
@RequestMapping("/excel")
public class ExcelController {
@Autowired
private MyExcelUtils myExcelUtils;
@PostMapping("/downloadExcel")
@CrossOrigin //跨域
public String downloadExcel(HttpServletResponse response, @RequestBody Map<String, Object> data) throws IOException {
return myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);
}
}
MyExcelUtils.java
package com.easyexcel.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.easyexcel.bo.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author Wulc
* @date 2023/7/25 17:07
* @description
*/
@Component
public class MyExcelUtils {
public File composeFile(Map<String, Object> map) throws IOException {
Resource resource = new ClassPathResource("/");
String path = resource.getFile().getPath();
String filePath = path + "excel.xlsx";
List<PeopleBO> peopleBOList = new ArrayList<>();
peopleBOList.add(new PeopleBO(map.get("name").toString(), map.get("age").toString(), map.get("sex").toString()));
EasyExcel.write(filePath, PeopleBO.class).sheet().useDefaultStyle(false).needHead(true).doWrite(peopleBOList);
return new File(filePath);
}
public String downloadExcel(File file, HttpServletResponse response) {
try {
// 获取文件名
String filename = file.getName();
// 获取文件后缀名
String ext = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
// 将文件写入输入流
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
response.reset();
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
response.addHeader("Content-Length", "" + file.length());
//跨域
response.addHeader("Access-Control-Allow-Origin", "*");
response.setContentType("application/octet-stream");
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
outputStream.write(buffer);
outputStream.flush();
outputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
} finally {
file.delete();
}
return "success";
}
}
文件是能成功下载了。
但后端报了一个错误
产生的原因,是因为返回了多次response了。因为一个接口只能有一个return,即有一个response响应给到调用方。但downloadExcel接口出现了两个response,但一个接口只能有一个response响应,因此另一个response就失效了,就会出现sendError()的报错。
解决方法有三种
1、输出流不要关闭(不推荐)
因为流一旦关闭,就意味着基本上就结束了对客户端的响应了。下面的return "success"就没法返回给调用方了。又因为是@RestController,需要一个可以封装成json的返回对象,显然“流”是不能封装成json的,@RestController需要下面的return "success",但你提前把“流”关闭了,return "success"不会响应,@RestController封装不到json对象,就会报错了。
2、controller接口或者util方法改为void(推荐)
不要return myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);
把return去掉,直接myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);
3、避免使用@RestController,改用@Controller
@RestController=@Controller+@ResponseBody,而@ResponseBody会把返回值封装成json的形式返回。如果不加@ResponseBody,则底层会把返回值封装成一个ModelAndView对像。显然文件流并不能封装成json,但由于通常在输出文件流后,会把这个流关闭,因此下面那个可以封装成json对象的return返回值就不能返回了。因此就会报错了。当然除非你一直让outputStream保持打开,使response响应不关闭。但不推荐这么做,文件流还是要用完及时关闭的。因为OutputStream也属于资源,处理完了以后务必要close()关闭并释放此流有关的所有系统资源,不然会大量占用系统内存资源,大量不释放资源会导致内存溢出。
4、总结
我们在jquery中常用的ajax其实就是对XMLHttpRequest进行了封装。ajax的底层就是XMLHttpRequest。jquery的出现主要就是为了更快捷的操作DOM,以及解决一些浏览器兼容性问题。jquery$.ajax通过对XHR(XMLHttpRequest简称XHR)封装,做了兼容性的处理,简化了使用,增加了对JSONP的支持。
JSONP类型可以支持跨域,因为jsonp不受同源策略的影响。所谓同源策略,”源“指的是:协议名(http/https)、域名/Ip地址、端口号。不同源的客户端/服务端,在没有对方授权的情况下是不允许发送/接收对方的数据资源的,会产生“跨域”情况。
JSONP用法举例:
前端
<script type="text/javascript" src="./js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#btnJSONP").click(function(){
var param={name:'zhangsan',age:'20',sex:'男'};
$.ajax({
url: 'http://localhost:6001/excel/testJsonP', // 跨域URL
type:'get',
dataType: 'jsonp',
jsonp:'jsoncallback',//自定义参数名称
jsonpCallback: 'showData', //指定回调函数名称
//timeout: 5000,
}).success(function(data){
console.log("success:"+data)
});
});
});
function showData(data){
console.info("回调showData:"+data);
}
</script>
<button class="btn" id="btnJSONP" name="btnJSONP">testJSONP</button>
后端
@GetMapping("/testJsonP")
public void testJsonP(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//前端传过来的回调函数名称
String callback = request.getParameter("jsoncallback");
//用回调函数名称包裹返回数据,这样,返回数据就作为回调函数的参数传回去了
String result = callback + "('Hello World')";
response.getWriter().write(result);
}
可以看到前后端不同源,但不用专门设置什么,就可以实现通信。这就是JSONP的跨域。
如果不用jsonp的话,用XMLHttpRequest或者ajax的话,则要设置一下
后端接口加上@CrossOrigin注解,设置response “Access-Control-Allow-Origin”请求头为“*”
response.addHeader("Access-Control-Allow-Origin", "*");
不然就会出现跨域报错:
除了XHR和ajax,在前端框架中广泛Http数据通信工具:fetch、axios。fetch和XMLHttpRequest一样都是底层的原生js,只不过Fetch是基于promise设计的适用于前端框架。
而ajax和axios都是封装了XMLHttpRequest,一个适用于jquery,一个则广泛运用于各种主流前端框架:vue、react等等。
5、参考资料
ajax跨域请求错误-CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource_ajax cors错误_我是一朵蒲公英的博客-CSDN博客
Ajax传JSON对象报错:JSON parse error: Unrecognized token ‘ids‘: was expecting (‘true‘, ‘false‘ or ‘null‘);_萌宅鹿同学的博客-CSDN博客
Can not construct instance of java.util.LinkedHashMap: no String-argument constructor/factory method_-droidcoffee-的博客-CSDN博客 【Java】解决POST表单提交报错 Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported_北宫清云的博客-CSDN博客
jQuery.ajax() | jQuery API Documentationjava 关闭输出流_Java OutputStream.close()关闭并释放输出流资源_卖糕郎的博客-CSDN博客
var,let,const三者的特点和区别_前端Vincent的博客-CSDN博客
已解决【Error】Cannot call sendError() after the response has been committed_hah杨大仙的博客-CSDN博客 springBoot文件下载出现 Cannot call sendError() after the response has been committed异常_木羊子羽的博客-CSDN博客
解决:java.lang.IllegalStateException: Cannot call sendError() after the response has been committed_java 转发 报cannot call sendredirect after the respon_郄子硕-langgeligelang的博客-CSDN博客 Controller和RestController的区别_controller与restcontroller区别_Linux资源站的博客-CSDN博客jsonp解决跨域问题_jsonp跨域_Ivymemphis的博客-CSDN博客
https://www.cnblogs.com/chiangchou/p/jsonp.html