Java实现文件下载

文章目录

pdf文件下载

在Spring后端使用Map封装了ResponseEntity<byte[]>的情况下,前端需要先解析这个Map结构,然后提取出文件数据进行下载。以下是完整的解决方案:

后端代码示例

java 复制代码
@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);
        
        // 创建ResponseEntity
        ResponseEntity<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<>());
        
        // 转换headers
        responseEntity.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处理

javascript 复制代码
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字符串转换为Blob
            const 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

javascript 复制代码
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处理方式

javascript 复制代码
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中:

java 复制代码
@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);
}

这样前端处理会更简单:

javascript 复制代码
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();
};

关键点总结

  1. Base64编码:后端需要对二进制数据进行Base64编码,避免JSON传输问题
  2. 头信息处理:需要正确解析Content-Type和Content-Disposition头
  3. Blob转换:前端需要将Base64字符串转换回Blob对象
  4. 文件名提取:从Content-Disposition头中正确提取文件名

选择哪种方案取决于你的具体需求和前后端约束条件。

Excel文件下载

当ResponseEntity里面放的是Excel文件时,处理方式需要特别注意文件类型和编码。以下是完整的解决方案:

后端代码示例

方案1:直接返回Excel流(推荐)

java 复制代码
@GetMapping("/download-excel")
public ResponseEntity<byte[]> downloadExcel() {
    try {
        // 创建Excel工作簿
        Workbook workbook = new XSSFWorkbook(); // 或 HSSFWorkbook for .xls
        Sheet 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中(按你的要求)

java 复制代码
@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到Map
        HttpHeaders 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<>());
        
        // 转换headers
        httpHeaders.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响应

javascript 复制代码
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处理方式

javascript 复制代码
const downloadExcelSimple = async () => {
    try {
        const response = await axios.post('/api/download-excel-map');
        const { status, body, filename } = response.data;
        
        if (status === 200) {
            // 直接使用Base64数据创建Blob
            const 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格式

javascript 复制代码
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库

bash 复制代码
npm install file-saver
javascript 复制代码
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);
    }
};

关键注意事项

  1. MIME类型:Excel文件的正确MIME类型是:

    • .xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
    • .xls: application/vnd.ms-excel
  2. 文件名编码:确保文件名支持中文,使用UTF-8编码

  3. 文件大小:对于大文件,考虑使用流式传输而不是Base64

  4. 错误处理:添加完整的错误处理机制

  5. 用户体验:可以添加下载进度提示和完成通知

这样处理可以确保Excel文件能够正确下载并在本地用Excel程序打开。

相关推荐
Yunfeng Peng2 小时前
1- 十大排序算法(选择排序、冒泡排序、插入排序)
java·算法·排序算法
暖木生晖2 小时前
Javascript变量介绍
开发语言·javascript·ecmascript
心.c2 小时前
JavaScript继承详讲
开发语言·javascript·ecmascript
立方世界2 小时前
【Leaflet.js实战】地图标记与弹窗:从基础到高级的完整实现
开发语言·javascript·ecmascript
断剑zou天涯3 小时前
【算法笔记】二叉树递归解题套路及其应用
java·笔记·算法
武子康4 小时前
Java-136 深入浅出 MySQL Spring Boot @Transactional 使用指南:事务传播、隔离级别与异常回滚策略
java·数据库·spring boot·mysql·性能优化·系统架构·事务
saber_andlibert4 小时前
【Linux】深入理解Linux的进程(一)
linux·运维·服务器·开发语言·c++
yanqiaofanhua7 小时前
C语言自学--数据在内存中的存储
c语言·开发语言
计算机软件程序设计10 小时前
基于Python的二手车价格数据分析与预测系统的设计与实现
开发语言·python·数据分析·预测系统