Ajax方式文件下载(后台返回为json或文件流)
在应用开发中,经常需要下载文件(如导出Excel),调用后台接口时,如果后台报错需要弹出错误信息,如果没有报错正常下载文件。本文主要介绍前台及后台(基于java)的处理方法,文中使用到的软件版本:Spring 4.3.9、Java 1.8.0_191、Jquery 1.12.4、Chrome 85.0.4183.121。
1、实现思路
方法一:通过原生Ajax响应头来区分是文本还是流,然后通过blob来处理返回数据
方法二:分成两个接口,先调用生成文件的接口,如果文件成功生成则再调用文件下载的接口;如果文件生成失败则弹出失败信息
2、方法一(Ajax响应头区分文本还是流)
2.1、后台
/** * 根据sql导成csv;成功返回文件流,失败返回错误信息 * @param sql 导出sql * @param col 字段信息,以逗号分隔 * @param title 文件头信息,以逗号分隔 * @param separator csv文件的分隔符 * @param baseServiceName * @param request * @param response */ @RequestMapping("exportForCsvStream") @ResponseBody public R<String> exportForCsvStream(String sql, String col, String title, String separator, String fileName, String baseServiceName, HttpServletRequest request, HttpServletResponse response) { logger.info("sql={},title={},separator={},fileName={},baseServiceName={}", sql, title, separator, fileName, baseServiceName); try { if (StringUtils.isBlank(separator)) { separator = ","; } if (StringUtils.isBlank(fileName)) { fileName = "data.csv"; } setResponse(fileName, request, response); OutputStream os = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
//调用service把文件写入到输出流,请自行实现 this.getBaseService(baseServiceName).exportForCsv(os, sql, col, title, separator); os.close(); return null; } catch(Exception e) { e.printStackTrace(); response.reset(); response.setContentType("application/json"); response.setHeader("Content-Disposition", ""); return R.error(-1, "发生异常"); } } private void setResponse(String fileName, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException { String urlFileName = ""; if (request.getHeader("User-Agent").toLowerCase().indexOf("firefox") > 0) { urlFileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1"); } else { urlFileName = java.net.URLEncoder.encode(fileName, "UTF-8"); } response.reset(); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\"" + urlFileName + "\""); response.setHeader("Connection", "close"); }
2.2、前台
2.2.1、拼接参数
function expForCsv() { let sql = "";//请自行设置 let col = "";//请自行设置 let title = "";//请自行设置
let fileName = "";//请自行设置
$.messager.progress({title : '处理中...'});//easyui控件,可用其他控件 let xhr = new XMLHttpRequest(); xhr.open("post", "${pageContext.request.contextPath}/common/export/exportForCsvStream.do", true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.responseType = "blob"; xhr.onload = function() {
$.messager.progress('close'); if (xhr.getResponseHeader("Content-type") == 'application/octet-stream') { let url = window.URL.createObjectURL(xhr.response); let a = document.createElement("a"); a.href = url; a.style.display = 'none' a.download = fileName; a.click(); window.URL.revokeObjectURL(url); a.remove(); } else { let reader = new FileReader(); reader.addEventListener('loadend', function (e) { let data = JSON.parse(e.target.result); alert('提示', "系统出错,错误信息为:" + data.description + ",请将该信息提供给代维人员寻求帮助"); }); reader.readAsText(xhr.response); } }; xhr.send('sql=' + sql + '&col=' + col + '&title=' + title + '&separator=|&t=' + new Date().getTime()); }
2.2.2、使用FormData
function expForCsv(type) {
let sql = "";//请自行设置 let col = "";//请自行设置 let title = "";//请自行设置 let fileName = ";//请自行设置
$.messager.progress({title : '处理中...'});//easyui控件,可用其他控件 let xhr = new XMLHttpRequest(); xhr.open("post", "${pageContext.request.contextPath}/common/export/exportForCsvStream.do", true); //xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.responseType = "blob"; xhr.onload = function() { $.messager.progress('close'); if (xhr.getResponseHeader("Content-type") == 'application/octet-stream') { let url = window.URL.createObjectURL(xhr.response); let a = document.createElement("a"); a.href = url; a.style.display = 'none' a.download = fileName; a.click(); window.URL.revokeObjectURL(url); a.remove(); } else { let reader = new FileReader(); reader.addEventListener('loadend', function (e) { let data = JSON.parse(e.target.result); alert('提示', "系统出错,错误信息为:" + data.description + ",请将该信息提供给代维人员寻求帮助"); }); reader.readAsText(xhr.response); } }; let data = new FormData(); data.append('sql', getCsql(type)); data.append('col', col); data.append('title', title); data.append('separator', '|'); data.append('t', new Date().getTime() + ""); xhr.send(data); }
3、方法二(两个接口)
3.1、后台
3.1.1、生成文件接口
/** * 根据sql导成csv;成功返回生成的文件名称,失败返回错误信息 * @param sql 导出sql * @param col 字段信息,以逗号分隔 * @param title 文件头信息,以逗号分隔 * @param separator csv文件分隔符 * @param baseServiceName * @return */ @RequestMapping("exportForCsv") @ResponseBody public CallResult<String> exportForCsv(String sql, String col, String title, String separator) { logger.info("sql={}", sql); logger.info("col={}", col); logger.info("title={}", title); logger.info("separator={}", separator); logger.info("baseServiceName={}", baseServiceName); OutputStream os = null; CallResult<String> result = new CallResult<>(); try { String fileName = "数据信息(" + DateUtil.getCurrentDateString("yyyyMMddHHmmsss") + ").xlsx"; os = new FileOutputStream(new File(WebconfigFactory.getTempFolder() + "数据信息(" + DateUtil.getCurrentDateString("yyyyMMddHHmmsss") + ").xlsx"));
//调用service生成文件,请自行实现 getService().exportForCsv(os, sql, col, title, separator); result.setResult(fileName); } catch(Exception e) { result = new CallResult<>(-1, "发生异常"); e.printStackTrace(); } finally { FileUtil.close(os); } return result; }
3.1.2、下载文件接口
/** * 下载方法 * 1.点击页面下载,需传参数: * fileName:磁盘上文件的名称,必需 * filePath或pathType或webPath: * 文件路径,传其中之一参数即可; * filePath是磁盘的绝对路径; * pathType是路径类型,对应webconfig.properties中的key * webPath是相对web应用的路径,如/spa * fileRealName:下载保存后的文件名称,可选;如果为空则与fileName一样 * * 2.后台导出文件下载,需传属性: * fileName:磁盘上文件的名称,必需 * fileRealName:下载保存后的文件名称,可选;如果为空则与fileName一样 */ @RequestMapping("download") public void download(HttpServletRequest request, HttpServletResponse response) { String filePath = ""; String fileName = request.getParameter("fileName"); String fileRealName = ""; if (!Util.isNull(fileName)) { filePath = request.getParameter("filePath"); if (Util.isNull(filePath)) { String webPath = request.getParameter("webPath"); if (Util.isNull(webPath)) { String pathType = request.getParameter("pathType"); filePath = WebconfigFactory.getValue(pathType); } else { //filePath = request.getRealPath(webPath) + System.getProperty("file.separator"); filePath = request.getServletContext().getRealPath(webPath) + System.getProperty("file.separator"); } } fileRealName = request.getParameter("fileRealName"); } else {//后台导出文件下载 fileName = (String) request.getAttribute("fileName"); filePath = WebconfigFactory.getTempFolder(); fileRealName = (String) request.getAttribute("fileRealName"); } if (Util.isNull(fileRealName)) { fileRealName = fileName; } BufferedInputStream bis = null; OutputStream out = null; try { String userInfoHeader = "User-Agent"; String firefoxName = "firefox"; String urlFileName = ""; if (request.getHeader(userInfoHeader).toLowerCase().indexOf(firefoxName) > 0) { urlFileName = new String(fileRealName.getBytes("UTF-8"), "ISO8859-1"); } else { urlFileName = java.net.URLEncoder.encode(fileRealName, "UTF-8"); } response.reset(); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment; filename=\"" + urlFileName + "\""); response.setHeader("Connection", "close"); bis = new BufferedInputStream(new FileInputStream(filePath + fileName), BUFFER_SIZE); out = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE); byte[] buf = new byte[BUFFER_SIZE]; int len; while ((len = bis.read(buf)) != -1) { out.write(buf, 0, len); } } catch (Exception e) { e.printStackTrace(); } finally { FileUtil.close(bis); FileUtil.close(out); } }
3.2、前台
function exp() { let sql = "";//请自行设置 let col = "";//请自行设置 let title = "";//请自行设置 $.messager.progress({title : '处理中...'});//easyui控件,可用其他控件 $.getJSON( "${pageContext.request.contextPath}/common/export/exportForCsv.do", { sql : sql, col : col, title : title,
separator: "|", t : new Date().getTime() }, function(data) { $.messager.progress('close');//easyui控件,可用其他控件 if (data.returnCode == 0) { location.href = "${pageContext.request.contextPath}/common/upDownload/download.do?fileName=" + encodeURI(data.result) + "&fileRealName=" + encodeURI("数据.csv") + "&pathType=path.temp.folder"; } else { $.messager.alert('提示', "系统出错,错误信息为:" + data.description + ",请将该信息提供给代维人员寻求帮助"); } } ); }
3、公共类R
package com.abc.common.entity; /** * 返回数据 */ public class R<T> { /** * 返回码 * 0 正常,其他异常 */ private int returnCode = 0; /** * 描述 */ private String description = "OK"; /** * 结果数据 */ private T result; public int getReturnCode() { return returnCode; } public String getDescription() { return description; } public T getResult() { return result; } public static R ok() { return new R(); } public static <T> R<T> ok(T result) { R<T> r = new R<>(); r.result = result; return r; } public static <T> R<T> error() { R<T> r = new R(); r.returnCode = -1; r.description = "未知异常,请联系管理员"; return r; } public static <T> R<T> error(String description) { R<T> r = new R(); r.returnCode = -1; r.description = description; return r; } public static <T> R<T> error(int returnCode, String description) { R<T> r = new R(); r.returnCode = returnCode; r.description = description; return r; } }
注意例子里sql是从台传到后台,这样容易引起SQL注入攻击;严谨的做法应该是前台传参数,后台拼sql,并使用PrepareStatement来执行sql。