Java实现文件下载
文章目录
- pdf文件下载
- 后端代码示例
- 前端处理方案
- 方案1:使用Axios处理
- 方案2:使用Fetch API
- 方案3:更简洁的Base64处理方式
- 后端优化建议
- 关键点总结
- Excel文件下载
- 后端代码示例
- 方案1:直接返回Excel流(推荐)
- 方案2:封装在Map中(按你的要求)
- 前端处理Excel下载
- 方案1:使用Axios处理Map响应
- 方案2:更简洁的Base64处理方式
- 方案3:处理不同Excel格式
- 使用第三方库简化处理
- 使用file-saver库
- 关键注意事项
pdf文件下载
在Spring后端使用Map
封装了ResponseEntity<byte[]>
的情况下,前端需要先解析这个Map结构,然后提取出文件数据进行下载。以下是完整的解决方案:
后端代码示例
@PostMapping("/download")
public Map<String, Object> downloadFile() {try {// 读取文件内容到byte数组byte[] fileContent = Files.readAllBytes(Paths.get("path/to/your/file.pdf"));// 设置响应头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_PDF);headers.setContentDispositionFormData("attachment", "filename.pdf");headers.setContentLength(fileContent.length);// 创建ResponseEntityResponseEntity<byte[]> responseEntity = new ResponseEntity<>(fileContent, headers, HttpStatus.OK);// 封装到Map中Map<String, Object> result = new HashMap<>();result.put("status", responseEntity.getStatusCodeValue());result.put("headers", new HashMap<>());// 转换headersresponseEntity.getHeaders().forEach((key, values) -> {if (values.size() > 0) {((Map<String, Object>) result.get("headers")).put(key, values.get(0));}});// 对文件内容进行Base64编码(避免JSON传输二进制数据问题)result.put("body", Base64.getEncoder().encodeToString(responseEntity.getBody()));return result;} catch (Exception e) {throw new RuntimeException("文件下载失败", e);}
}
前端处理方案
方案1:使用Axios处理
import axios from 'axios';const downloadFile = async () => {try {const response = await axios.post('/api/download', {}, {responseType: 'json' // 重要:先以JSON格式接收});const { status, headers, body } = response.data;if (status === 200) {// 将Base64字符串转换为Blobconst binaryString = atob(body);const bytes = new Uint8Array(binaryString.length);for (let i = 0; i < binaryString.length; i++) {bytes[i] = binaryString.charCodeAt(i);}const blob = new Blob([bytes], { type: headers['content-type'] || 'application/octet-stream' });// 创建下载链接const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;// 从Content-Disposition头中获取文件名const contentDisposition = headers['content-disposition'];let filename = 'download.pdf';if (contentDisposition) {const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);if (filenameMatch) {filename = filenameMatch[1];}}a.download = filename;document.body.appendChild(a);a.click();document.body.removeChild(a);window.URL.revokeObjectURL(url);}} catch (error) {console.error('下载失败:', error);}
};
方案2:使用Fetch API
const downloadFileWithFetch = async () => {try {const response = await fetch('/api/download', {method: 'POST',headers: {'Content-Type': 'application/json'}});const data = await response.json();if (data.status === 200) {// 处理Base64数据const byteCharacters = atob(data.body);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);const blob = new Blob([byteArray], { type: data.headers['content-type'] });// 下载文件const url = URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;// 获取文件名const disposition = data.headers['content-disposition'];const filename = disposition ? disposition.split('filename=')[1].replace(/"/g, '') : 'file.pdf';link.download = filename;link.click();URL.revokeObjectURL(url);}} catch (error) {console.error('下载失败:', error);}
};
方案3:更简洁的Base64处理方式
const downloadFileSimplified = async () => {try {const response = await axios.post('/api/download');const { status, headers, body } = response.data;if (status === 200) {// 直接使用fetch下载Base64数据const blob = await fetch(`data:${headers['content-type']};base64,${body}`).then(res => res.blob());const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = getFilenameFromHeaders(headers) || 'file';a.click();URL.revokeObjectURL(url);}} catch (error) {console.error('下载失败:', error);}
};// 从headers中提取文件名的辅助函数
const getFilenameFromHeaders = (headers) => {const disposition = headers['content-disposition'];if (!disposition) return null;const utf8Filename = disposition.match(/filename\*=UTF-8''(.+)/);if (utf8Filename) {return decodeURIComponent(utf8Filename[1]);}const filename = disposition.match(/filename="?(.+)"?/);return filename ? filename[1].replace(/"/g, '') : null;
};
后端优化建议
如果可能,建议后端直接返回ResponseEntity<byte[]>
而不是封装在Map中:
@PostMapping("/download-direct")
public ResponseEntity<byte[]> downloadFileDirect() {byte[] fileContent = // ... 读取文件HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_PDF);headers.setContentDispositionFormData("attachment", "filename.pdf");return new ResponseEntity<>(fileContent, headers, HttpStatus.OK);
}
这样前端处理会更简单:
const downloadDirect = async () => {const response = await axios.post('/api/download-direct', {}, {responseType: 'blob' // 直接接收Blob数据});const url = window.URL.createObjectURL(new Blob([response.data]));const link = document.createElement('a');link.href = url;link.setAttribute('download', 'filename.pdf');document.body.appendChild(link);link.click();link.remove();
};
关键点总结
- Base64编码:后端需要对二进制数据进行Base64编码,避免JSON传输问题
- 头信息处理:需要正确解析Content-Type和Content-Disposition头
- Blob转换:前端需要将Base64字符串转换回Blob对象
- 文件名提取:从Content-Disposition头中正确提取文件名
选择哪种方案取决于你的具体需求和前后端约束条件。
Excel文件下载
当ResponseEntity里面放的是Excel文件时,处理方式需要特别注意文件类型和编码。以下是完整的解决方案:
后端代码示例
方案1:直接返回Excel流(推荐)
@GetMapping("/download-excel")
public ResponseEntity<byte[]> downloadExcel() {try {// 创建Excel工作簿Workbook workbook = new XSSFWorkbook(); // 或 HSSFWorkbook for .xlsSheet sheet = workbook.createSheet("数据报表");// 创建表头Row headerRow = sheet.createRow(0);headerRow.createCell(0).setCellValue("姓名");headerRow.createCell(1).setCellValue("年龄");headerRow.createCell(2).setCellValue("部门");// 添加数据Row dataRow = sheet.createRow(1);dataRow.createCell(0).setCellValue("张三");dataRow.createCell(1).setCellValue(28);dataRow.createCell(2).setCellValue("技术部");// 写入字节数组ByteArrayOutputStream outputStream = new ByteArrayOutputStream();workbook.write(outputStream);workbook.close();byte[] excelBytes = outputStream.toByteArray();// 设置响应头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.setContentDisposition(ContentDisposition.builder("attachment").filename("数据报表.xlsx", StandardCharsets.UTF_8).build());headers.setContentLength(excelBytes.length);return new ResponseEntity<>(excelBytes, headers, HttpStatus.OK);} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}
}
方案2:封装在Map中(按你的要求)
@PostMapping("/download-excel-map")
public Map<String, Object> downloadExcelInMap() {try {// 创建Excel文件Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet("数据报表");// 填充数据...Row headerRow = sheet.createRow(0);String[] headers = {"ID", "名称", "数量", "价格"};for (int i = 0; i < headers.length; i++) {headerRow.createCell(i).setCellValue(headers[i]);}// 示例数据Object[][] data = {{1, "商品A", 100, 29.99},{2, "商品B", 50, 39.99},{3, "商品C", 75, 19.99}};for (int i = 0; i < data.length; i++) {Row row = sheet.createRow(i + 1);for (int j = 0; j < data[i].length; j++) {if (data[i][j] instanceof Number) {row.createCell(j).setCellValue(((Number) data[i][j]).doubleValue());} else {row.createCell(j).setCellValue(data[i][j].toString());}}}// 转换为字节数组ByteArrayOutputStream outputStream = new ByteArrayOutputStream();workbook.write(outputStream);workbook.close();byte[] excelBytes = outputStream.toByteArray();// 封装ResponseEntity到MapHttpHeaders httpHeaders = new HttpHeaders();httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);httpHeaders.setContentDisposition(ContentDisposition.builder("attachment").filename("商品数据.xlsx", StandardCharsets.UTF_8).build());Map<String, Object> result = new HashMap<>();result.put("status", HttpStatus.OK.value());result.put("headers", new HashMap<>());// 转换headershttpHeaders.forEach((key, values) -> {if (!values.isEmpty()) {((Map<String, Object>) result.get("headers")).put(key, values.get(0));}});// Excel文件使用Base64编码result.put("body", Base64.getEncoder().encodeToString(excelBytes));result.put("filename", "商品数据.xlsx"); // 额外添加文件名字段return result;} catch (Exception e) {Map<String, Object> errorResult = new HashMap<>();errorResult.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());errorResult.put("error", e.getMessage());return errorResult;}
}
前端处理Excel下载
方案1:使用Axios处理Map响应
import axios from 'axios';const downloadExcel = async () => {try {const response = await axios.post('/api/download-excel-map', {}, {responseType: 'json'});const { status, headers, body, filename } = response.data;if (status === 200) {// 处理Base64编码的Excel数据const byteCharacters = atob(body);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);const blob = new Blob([byteArray], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });// 创建下载链接const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;// 获取文件名(优先使用后端返回的filename字段,其次从headers中解析)let downloadFilename = filename;if (!downloadFilename && headers['content-disposition']) {const match = headers['content-disposition'].match(/filename="?(.+)"?/);downloadFilename = match ? match[1] : 'download.xlsx';}link.download = downloadFilename || 'data.xlsx';document.body.appendChild(link);link.click();document.body.removeChild(link);window.URL.revokeObjectURL(url);console.log('Excel文件下载完成');} else {console.error('下载失败:', response.data.error);}} catch (error) {console.error('请求失败:', error);}
};
方案2:更简洁的Base64处理方式
const downloadExcelSimple = async () => {try {const response = await axios.post('/api/download-excel-map');const { status, body, filename } = response.data;if (status === 200) {// 直接使用Base64数据创建Blobconst blob = base64ToBlob(body, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = filename || 'excel_data.xlsx';a.style.display = 'none';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);}} catch (error) {console.error('下载失败:', error);}
};// Base64转Blob的辅助函数
const base64ToBlob = (base64, contentType = '', sliceSize = 512) => {const byteCharacters = atob(base64);const byteArrays = [];for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {const slice = byteCharacters.slice(offset, offset + sliceSize);const byteNumbers = new Array(slice.length);for (let i = 0; i < slice.length; i++) {byteNumbers[i] = slice.charCodeAt(i);}const byteArray = new Uint8Array(byteNumbers);byteArrays.push(byteArray);}return new Blob(byteArrays, { type: contentType });
};
方案3:处理不同Excel格式
const downloadExcelAdvanced = async () => {try {const response = await axios.post('/api/download-excel-map');const { status, headers, body, filename } = response.data;if (status !== 200) {throw new Error(`下载失败: ${response.data.error}`);}// 根据文件扩展名确定MIME类型const fileExt = filename ? filename.split('.').pop().toLowerCase() : 'xlsx';let mimeType;switch (fileExt) {case 'xls':mimeType = 'application/vnd.ms-excel';break;case 'xlsx':default:mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';}// 转换并下载const blob = base64ToBlob(body, mimeType);downloadBlob(blob, filename || `data.${fileExt}`);} catch (error) {console.error('Excel下载错误:', error);// 可以添加用户通知,如Toast提示alert('文件下载失败,请重试');}
};// 通用的Blob下载函数
const downloadBlob = (blob, filename) => {const url = URL.createObjectURL(blob);const link = document.createElement('a');// 针对不同浏览器的下载兼容性处理if (typeof link.download === 'undefined') {window.open(url);} else {link.href = url;link.download = filename;link.style.display = 'none';document.body.appendChild(link);link.click();document.body.removeChild(link);}// 清理URL对象setTimeout(() => URL.revokeObjectURL(url), 100);
};
使用第三方库简化处理
使用file-saver库
npm install file-saver
import { saveAs } from 'file-saver';const downloadExcelWithFileSaver = async () => {try {const response = await axios.post('/api/download-excel-map');const { status, body, filename } = response.data;if (status === 200) {const byteCharacters = atob(body);const byteNumbers = new Uint8Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {byteNumbers[i] = byteCharacters.charCodeAt(i);}const blob = new Blob([byteNumbers], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });saveAs(blob, filename || 'download.xlsx');}} catch (error) {console.error('下载失败:', error);}
};
关键注意事项
-
MIME类型:Excel文件的正确MIME类型是:
.xlsx
:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
.xls
:application/vnd.ms-excel
-
文件名编码:确保文件名支持中文,使用UTF-8编码
-
文件大小:对于大文件,考虑使用流式传输而不是Base64
-
错误处理:添加完整的错误处理机制
-
用户体验:可以添加下载进度提示和完成通知
这样处理可以确保Excel文件能够正确下载并在本地用Excel程序打开。