Use XMLHttpRequest to realize asynchronous file download

1. Problem description

I want to implement the download culture asynchronously, and the request is a post request. At first I planned to use 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 = 'table.xlsx';
                    a. click();
                    URL.revokeObjectURL(url1);

        });

However, the return type of ajax does not support binary file stream (binary)! Therefore, the asynchronous method of ajax cannot receive the file stream returned by the back-end interface, and the file cannot be downloaded.

jQuery.ajax() | jQuery API Documentation

2. Solution

Use dom’s native XMLHttpRequest instead.

The return type of XMLHttpRequest is binary data blob, which can be connected to a file stream.

XMLHttpRequest.responseType – Web API Interface Reference | MDN (mozilla.org)

3. Code example

3.1, front-end code

downloadExcel.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script type="text/javascript" src="./js/jquery.min.js"></script>
\t\t
<script type="text/javascript">
$(document). ready(function() {
$("#btnDownload").click(function(){
var param={name:'zhangsan',age:'20',sex:'male'};
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'; // Set the downloaded file name
downloadLink. click();
}
\t\t\t\t\t\t\t
}
}
});
});
</script>
\t\t
<button class="btn" id="btnDownload" name="btnDownload">Download file</button>
</body>
</html>

3.2, backend code

<strong>ExcelController.java</strong>
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 //cross domain
    public String downloadExcel(HttpServletResponse response, @RequestBody Map<String, Object> data) throws IOException {
        return myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);
    }
}
<strong>MyExcelUtils.java</strong>
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 {
            // get the file name
            String filename = file. getName();
            // Get the file extension
            String ext = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
            // write the file to the input stream
            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());
            //Cross domain
            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";
    }
}

The file can be downloaded successfully.

But the backend reported an error

The reason for this is that multiple responses have been returned. Because an interface can only have one return, that is, one response is given to the caller. But there are two responses in the downloadExcel interface, but one interface can only have one response response, so the other response becomes invalid, and an error of sendError() will appear.

There are three solutions

1. Do not close the output stream (not recommended)

Because once the stream is closed, it means that the response to the client is basically over. The following return “success” cannot be returned to the caller. And because it is @RestController, it needs a return object that can be encapsulated into json. Obviously, “stream” cannot be encapsulated into json. @RestController needs the following return “success”, but you closed the “stream” in advance, return “success” will not respond, and @RestController will report an error if it cannot encapsulate the json object.

2. Change the controller interface or util method to void (recommended)

Don’t return myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);

Remove return, directly myExcelUtils.downloadExcel(myExcelUtils.composeFile(data), response);

3. Avoid using @RestController, use @Controller instead

@RestController=@Controller + @ResponseBody, and @ResponseBody will return the return value in the form of json. If you don’t add @ResponseBody, the bottom layer will encapsulate the return value into a ModelAndView object. Obviously, the file stream cannot be encapsulated into json, but since the stream is usually closed after outputting the file stream, the following return return value that can be encapsulated into a json object cannot be returned. So it will report an error. Of course, unless you keep the outputStream open all the time, so that the response response is not closed. But it is not recommended to do so, the file stream should be closed in time when it is used up. Because OutputStream is also a resource, after processing, you must close() to close and release all system resources related to this stream, otherwise it will occupy a large amount of system memory resources, and if a large number of resources are not released, memory overflow will result.

4. Summary

The ajax we commonly use in jquery is actually an encapsulation of XMLHttpRequest. The bottom layer of ajax is XMLHttpRequest. The emergence of jquery is mainly to operate the DOM more quickly and solve some browser compatibility problems. jquery$.ajax encapsulates XHR (XMLHttpRequest for short), handles compatibility, simplifies usage, and adds support for JSONP.

The JSONP type can support cross-domain, because jsonp is not affected by the same-origin policy. The so-called same-origin policy, “source” refers to: protocol name (http/https), domain name/Ip address, port number. Clients/servers from different sources are not allowed to send/receive the data resources of the other party without the authorization of the other party, which will cause “cross-domain” situations.

Examples of JSONP usage:

front end

<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:'male'};
$.ajax({
url: 'http://localhost:6001/excel/testJsonP', // cross-domain URL
type:'get',
dataType: 'jsonp',
jsonp:'jsoncallback',//custom parameter name
jsonpCallback: 'showData', //Specify the callback function name
//timeout: 5000,
}).success(function(data){
console.log("success:" + data)
});
});
            });
function showData(data){
console.info("Callback showData:" + data);
}
\t\t\t\t
</script>

<button class="btn" id="btnJSONP" name="btnJSONP">testJSONP</button>

rear end

 @GetMapping("/testJsonP")
 public void testJsonP(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        //The name of the callback function passed from the front end
        String callback = request. getParameter("jsoncallback");
        //Wrap the returned data with the name of the callback function, so that the returned data is passed back as a parameter of the callback function
        String result = callback + "('Hello World')";
        response.getWriter().write(result);
    }

It can be seen that the front and back ends have different sources, but communication can be realized without any special settings. This is the cross domain of JSONP.

If you don’t use jsonp, use XMLHttpRequest or ajax, you need to set it

Add the @CrossOrigin annotation to the backend interface, and set the response “Access-Control-Allow-Origin” request header to “*”

response.addHeader("Access-Control-Allow-Origin", "*");

Otherwise, a cross-domain error will appear:

In addition to XHR and ajax, Http data communication tools are widely used in the front-end framework: fetch, axios. Fetch and XMLHttpRequest are the same underlying native js, but Fetch is designed based on promise and is suitable for front-end frameworks.

Both ajax and axios encapsulate XMLHttpRequest, one is suitable for jquery, and the other is widely used in various mainstream front-end frameworks: vue, react, etc.

5. Reference

Ajax cross domain request error – CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource_ajax cors error_I am a dandelion blog-CSDN blog

Ajax passes JSON object error: JSON parse error: Unrecognized token ‘ids’: was expecting (‘true’, ‘false’ or ‘null’);

Can not construct instance of java.util.LinkedHashMap: no String-argument constructor/factory method_-droidcoffee-的博客-CSDN Blog【Java】Solve POST form submission error Content type ‘application/x-www-form-urlencoded;charset =UTF-8’ not supported_Beimong Qingyun’s Blog-CSDN Blog

jQuery.ajax() | jQuery API Documentationjava closes the output stream_Java OutputStream.close() closes and releases the output stream resource_Sell Cake Lang’s Blog-CSDN Blog

The characteristics and differences of var, let, and const – front-end Vincent’s Blog – CSDN Blog

Solved [Error] Cannot call sendError() after the response has been committed_hah Yang Daxian’s blog-CSDN blog springBoot file download appears Cannot call sendError() after the response has been committed exception_Muyangziyu’s Blog-CSDN Blog

Solution: java.lang.IllegalStateException: Cannot call sendError() after the response has been committed_java forwarding report cannot call sendredirect after the response Blog-CSDN blog jsonp solves cross-domain problems_jsonp cross-domain_Ivymemphis’s blog-CSDN blog

https://www.cnblogs.com/chiangchou/p/jsonp.html