You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot+Next.js列表导出Excel:最佳实践与接口配置疑问

问题与解决方案

问题描述

我正在实现Next.js前端点击按钮导出列表为Excel文件的功能,后端基于Spring Boot开发,需通过后端接口返回文件供用户下载。

我的疑问:

  1. 该场景下,实现按钮点击下载文件的最佳实践是什么?
  2. 后端接口的返回类型与Content-Type应如何设置?

我已尝试修改Content-Type与MediaType,但似乎在接口的返回类型和Content-Type设置上出现了问题。


现有后端代码

ExcelExporter.java

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFFont;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

public class ExcelExporter {
    private final XSSFWorkbook workbook;
    private XSSFSheet sheet;
    private final List<Debt> debts;

    String[] columns = {"Reference", "Title", "Descrip", "Gateway", "Status", "Amount (USD)", "Paid", "Name"};

    public ExcelExporter(List<Debt> debts) {
        this.debts = debts;
        workbook = new XSSFWorkbook();
    }


    private void writeHeaderLine() {
        sheet = workbook.createSheet("Payment Report");

        CellStyle headerCellStyle = workbook.createCellStyle();

        // 设置表头字体
        XSSFFont headerFont = workbook.createFont();
        headerFont.setBold(true);
        headerFont.setFontHeight(16);

        // 设置表头单元格样式
        headerCellStyle.setFont(headerFont);

        // 创建表头行
        Row headerRow = sheet.createRow(0);

        for(int col=0; col <columns.length; col++){
            createCell(headerRow, col, columns[col], headerCellStyle);
        }

    }

    private void createCell(Row row, int columnCount, String value, CellStyle style) {
        sheet.autoSizeColumn(columnCount);
        Cell cell = row.createCell(columnCount);
        cell.setCellValue(value);
        cell.setCellStyle(style);
    }

    private void writeDataLines() {
        int rowIdx = 1;

        CellStyle style = workbook.createCellStyle();
        XSSFFont font = workbook.createFont();
        font.setFontHeight(14);
        style.setFont(font);

        for (Debt debt : debts) {
            Row dataRow = sheet.createRow(rowIdx++);
            int columnCount = 0;

            createCell(dataRow, columnCount++, debt.getRef(), style);
            createCell(dataRow, columnCount++, debt.getTitle(), style);
            createCell(dataRow, columnCount++, debt.getDescrip(), style);
            createCell(dataRow, columnCount++, debt.getGateway(), style);
            createCell(dataRow, columnCount++, debt.getStat(), style);
            createCell(dataRow, columnCount++, debt.getAmount(), style);
            createCell(dataRow, columnCount++, debt.getPaid(), style);
            createCell(dataRow, columnCount++, debt.getName(), style);

        }
    }

    public ByteArrayInputStream export() throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try {
            writeHeaderLine();
            writeDataLines();
            workbook.write(outputStream);
            return new ByteArrayInputStream(outputStream.toByteArray());
        }catch (IOException e){
            throw new RuntimeException("导出Excel文件失败: " + e.getMessage());
        }

    }
}

ReportController.java

import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("report")
public class ReportController {

    @GetMapping(value = "export-to-excel", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public byte[] exportReport() throws IOException {

        List<Debt> debtSummaries = new ArrayList<Debt>();
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        
        DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
        String currentDateTime = dateFormatter.format(new Date());

        String headerKey = HttpHeaders.CONTENT_DISPOSITION;
        String headerValue = "attachment; filename=PaymentReport_" + currentDateTime + ".xlsx";
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(headerKey, headerValue);

        ExcelExporter excelExporter = new ExcelExporter(debtSummaries);
        ByteArrayInputStream in = excelExporter.export();


        return IOUtils.toByteArray(in);
}

Debt.java

@Getter
@Setter
@AllArgsConstructor
public class Debt {

    String ref;

    String title;

    String descrip;

    String gateway;

    String stat;

    String amount;

    String paid;

    String name;
}

解决方案

1. 前端按钮点击下载的最佳实践

两种常用方案,按需选择:

方案一:直接跳转/使用<a>标签(适合无参数的GET请求)

无需额外JS逻辑,依赖浏览器原生下载能力,简单高效:

{/* 按钮触发跳转 */}
<button onClick={() => window.open('/report/export-to-excel', '_self')}>
  导出Excel
</button>

{/* 或者直接用a标签 */}
<a href="/report/export-to-excel" download>导出Excel</a>

方案二:Fetch/Axios请求后手动触发下载(适合传参、POST请求或错误处理场景)

需要传递筛选参数、用POST请求,或要处理导出失败时,用这种方式:

const handleExport = async () => {
  try {
    const response = await fetch('/report/export-to-excel');
    if (!response.ok) throw new Error('导出请求失败');
    
    const blob = await response.blob();
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    
    // 从响应头获取后端指定的文件名
    const contentDisposition = response.headers.get('Content-Disposition');
    let filename = 'PaymentReport.xlsx';
    if (contentDisposition) {
      const match = contentDisposition.match(/filename="?([^"]+)"?/);
      if (match) filename = match[1];
    }
    
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    
    // 清理资源
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  } catch (error) {
    alert(`导出失败: ${error.message}`);
  }
};

// 使用按钮
<button onClick={handleExport}>导出Excel</button>

2. 后端接口的返回类型与Content-Type设置

你的现有代码问题在于:创建了HttpHeaders但未与响应内容绑定返回,导致浏览器无法正确识别下载文件。正确实现如下:

修正后的ReportController代码

import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("report")
public class ReportController {

    @GetMapping("export-to-excel")
    public ResponseEntity<byte[]> exportReport() throws IOException {
        List<Debt> debtSummaries = new ArrayList<>();
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        debtSummaries.add(new Debt("AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH"));
        
        DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
        String currentDateTime = dateFormatter.format(new Date());
        String filename = "PaymentReport_" + currentDateTime + ".xlsx";

        // 生成Excel文件
        ExcelExporter excelExporter = new ExcelExporter(debtSummaries);
        ByteArrayInputStream in = excelExporter.export();
        byte[] excelBytes = IOUtils.toByteArray(in);

        // 设置响应头
        HttpHeaders headers = new HttpHeaders();
        // 指定文件为附件下载,设置文件名
        headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"");
        // 设置精确的Content-Type,比通用二进制流更友好
        headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));

        // 返回ResponseEntity,绑定字节数组、头信息和状态码
        return new ResponseEntity<>(excelBytes, headers, HttpStatus.OK);
    }
}

关键设置说明

  • 返回类型:必须使用ResponseEntity<byte[]>,才能将自定义响应头(如Content-Disposition)与文件字节流一同返回。直接返回byte[]时,Spring不会自动添加你创建的头信息。
  • Content-Type:.xlsx文件推荐使用精确MIME类型application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,浏览器能更好识别文件类型;用MediaType.APPLICATION_OCTET_STREAM_VALUE也能下载,但类型识别精度较低。
  • Content-Disposition:必须设置为attachment; filename="xxx.xlsx",告诉浏览器这是需下载的附件,并指定文件名,避免下载后文件名混乱。

内容的提问来源于stack exchange,提问作者Imran Zakhayev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 23:10:30