使用SpringBoot+Apache POI导出Word的流程
1、依赖配置
在pom.xml
中添加Apache POI依赖:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
2、工具类
这里提供了导出用到的工具类,可以仔细看一下具体内容:
public class WordUtil {
/**
* 合并横向单元格(跨列合并)
* @param table 目标表格
* @param row 行号(从0开始)
* @param fromCol 起始列号(从0开始)
* @param toCol 结束列号(从0开始)
*/
public static void mergeCellsHorizontal(XWPFTable table, int row, int fromCol, int toCol) {
for (int colIndex = fromCol; colIndex <= toCol; colIndex++) {
XWPFTableCell cell = table.getRow(row).getCell(colIndex);
if (cell == null) continue; // 防止空指针异常
if (colIndex == fromCol) {
// 起始列标记为合并起点
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.RESTART);
} else {
// 后续列标记为合并延续
cell.getCTTc().addNewTcPr().addNewHMerge().setVal(STMerge.CONTINUE);
}
}
}
/**
* 合并纵向相同的数据
*
* @param table 要合并单元格的表格
* @param col 要合并的列索引
* 注意:
* 1、该方法会修改表格的结构,可能会导致行的高度发生变化。
* 2、该方法只是根据相邻单元getCellValue()值相同进行合并
*/
public static void mergeVerticalCells(XWPFTable table, int col) {
int rowCount = table.getNumberOfRows();
if (rowCount < 2) {
return; // 如果表格行数小于2,无需合并
}
String previousValue = getCellValue(table.getRow(0).getCell(col));
int startRow = 0;
for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) {
String currentValue = getCellValue(table.getRow(rowIndex).getCell(col));
if (!currentValue.equals(previousValue)) {
if (rowIndex - 1 > startRow) {
mergeCellsVertically(table, col, startRow, rowIndex - 1);
}
startRow = rowIndex;
previousValue = currentValue;
}
}
if (rowCount - 1 > startRow) {
mergeCellsVertically(table, col, startRow, rowCount - 1);
}
}
/**
* 跨行合并单元格
*
* @param table 要合并单元格的表格
* @param col 要合并的列索引
* @param fromRow 从哪一行开始合并单元格
* @param toRow 合并到哪一行
*/
public static void mergeCellsVertically(XWPFTable table, int col, int fromRow, int toRow) {
for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++) {
XWPFTableCell cell = table.getRow(rowIndex).getCell(col);
if (rowIndex == fromRow) {
// The first merged cell is set with RESTART merge value
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.RESTART);
} else {
// Cells which join (merge) the first one, are set with CONTINUE
cell.getCTTc().addNewTcPr().addNewVMerge().setVal(STMerge.CONTINUE);
}
}
}
/**
* 获取单元格的文本值
*
* @param cell 单元格
* @return 单元格的文本值
*/
public static String getCellValue(XWPFTableCell cell) {
List<XWPFParagraph> paragraphs = cell.getParagraphs();
if (paragraphs != null && !paragraphs.isEmpty()) {
return paragraphs.get(0).getText();
}
return "";
}
/**
* 设置字体颜色
*
* @param cell 单元格
* @param textPart1 第一部分文本
* @param textPart2 第二部分文本
* @param isRed 是否红色
* @return 单元格的文本值
*/
public static void setCellTextWithColor(XWPFTableCell cell, String textPart1, String textPart2, boolean isRed) {
XWPFParagraph paragraph = !cell.getParagraphs().isEmpty() ? cell.getParagraphArray(0) : cell.addParagraph();
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 第一部分文本(属性:)
XWPFRun run1 = paragraph.createRun();
if (isRed) {
run1.setColor("FF0000"); // 设置红色
}
run1.setText(textPart1);
// 第二部分文本(实际属性值或"属性值:")
XWPFRun run2 = paragraph.createRun();
run2.setText(textPart2);
}
/**
* 设置表格单元格文本并居中对齐
* @param cell 表格单元格
* text 文本内容
* **/
public static void setCellTextAndAlignment(XWPFTableCell cell, String text) {
// 在单元格中创建段落
XWPFParagraph paragraph = !cell.getParagraphs().isEmpty() ? cell.getParagraphArray(0) : cell.addParagraph();
// 设置段落对齐方式为居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 创建运行并设置文本
XWPFRun run = paragraph.createRun();
run.setText(text);
}
/**
* 修改表格的样式
*
* @param table 表格
*/
public static void tableStyle(XWPFTable table) {
// 获取表格属性对象
CTTblPr tblPr = table.getCTTbl().getTblPr();
if (tblPr == null) {
tblPr = table.getCTTbl().addNewTblPr();
}
if (!tblPr.isSetJc()) {
tblPr.addNewJc(); // 确保存在对齐属性节点
}
CTJcTable jc = tblPr.getJc();
jc.setVal(STJcTable.CENTER);
// 设置表格宽度和自适应布局
CTTblWidth tblWidth = tblPr.addNewTblW();
tblWidth.setType(STTblWidth.DXA);
tblWidth.setW(BigInteger.valueOf(8300)); // 设置固定宽度(单位:DXA)
//设置单元格和其文本样式
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
// 获取单元格的属性
if (cell.getCTTc().getTcPr() == null) {
cell.getCTTc().addNewTcPr();
}
CTTcMar cellMar = cell.getCTTc().getTcPr().isSetTcMar()
? cell.getCTTc().getTcPr().getTcMar()
: cell.getCTTc().getTcPr().addNewTcMar();
// 设置内边距
cellMar.addNewLeft().setW("100");
cellMar.addNewRight().setW("100");
cellMar.addNewTop().setW("100");
cellMar.addNewBottom().setW("100");
// 设置单元格垂直居中
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
// 遍历单元格内的所有段落
for (XWPFParagraph paragraph : cell.getParagraphs()) {
// 设置水平居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 段后0磅
paragraph.setSpacingAfter(0);
// 3. 设置行距为多倍行距1(关键步骤)
CTSpacing spacing = paragraph.getCTP().getPPr().isSetSpacing()
? paragraph.getCTP().getPPr().getSpacing()
: paragraph.getCTP().getPPr().addNewSpacing();
// 多倍行距1(相当于Word里的1倍行距)
spacing.setLineRule(STLineSpacingRule.Enum.forInt(LineSpacingRule.AUTO.getValue())); // AUTO = 多倍行距
spacing.setLine(BigInteger.valueOf(240)); // 240 = 1倍行距(标准值)
//设置文本样式
for (XWPFRun run : paragraph.getRuns()) {
// 设置字体为宋体(需确认字体名称)
run.setFontFamily("宋体");
// 设置字号为12磅
run.setFontSize(12);
}
}
}
}
}
/**
* 插入一级标题并修改其样式
*
* @param doc,text word文档和对应的内容
*/
public static void addContentParagraphTitle(XWPFDocument doc, String text) {
XWPFParagraph para = doc.createParagraph();
para.setSpacingBetween(1.0f); //行间距1.0
XWPFRun run = para.createRun();
run.setFontFamily("宋体");
run.setFontSize(16);
run.setText(text);
}
/**
* 插入正文文本并修改其样式
*
* @param doc,text word文档和对应的内容
*/
public static void addContentParagraphText(XWPFDocument doc, String text) {
XWPFParagraph para = doc.createParagraph();
para.setIndentationFirstLine(500); //首航缩进2个空格
para.setSpacingBetween(1.0f); //行间距1.0
XWPFRun run = para.createRun();
run.setFontFamily("宋体");
run.setFontSize(14);
run.setText(text);
}
}
3、导出接口内容
这里是完整的接口内容:
@RestController
@RequestMapping("/export")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class ExportWordController {
@PostMapping("/word")
public void genera(HttpServletResponse response) throws IOException {
try {
// 1. 设置 CORS 头(根据实际前端地址调整)
response.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
// 2. 设置下载头(编码文件名)
String encodedFileName = "";
encodedFileName = URLEncoder.encode("word导出.docx", "UTF-8").replace("+", "%20");
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName);
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
// 3. 生成文档
XWPFDocument doc = new XWPFDocument();
//文本内容
setText(doc);
//表格内容
setTable(doc);
//图片内容
setImage(doc);
// 4. 写入响应流
OutputStream out = response.getOutputStream();
doc.write(out);
} catch (Exception e) {
response.sendError(500, "文件生成失败: " + e.getMessage());
}
}
public void setText(XWPFDocument doc){
// 主标题(宋体、三号、居中)
XWPFParagraph titlePara = doc.createParagraph();
titlePara.setSpacingBetween(1.0f); //行间距1.0
titlePara.setAlignment(ParagraphAlignment.CENTER);
XWPFRun titleRun = titlePara.createRun();
titleRun.setFontFamily("宋体");
titleRun.setFontSize(18);
titleRun.setText("word标题");
WordUtil.addContentParagraphTitle(doc, "一、文本");//一级标题
WordUtil.addContentParagraphText(doc, "这是一段文字描述");//正文
}
public void setTable(XWPFDocument doc){
//构建表格内容
List<StudentClass> list = new ArrayList<>();
//添加数据
list.add(new StudentClass("一班", List.of(new Student("张三"), new Student("李四"))));
list.add(new StudentClass("二班", List.of(new Student("王五"), new Student("赵六"))));
WordUtil.addContentParagraphTitle(doc, "二、表格"); // 一级标题
//创建表格
XWPFTable table = doc.createTable();
XWPFTableRow tableHeader = table.getRow(0); // 获取第一行(默认已有一行)
WordUtil.setCellTextAndAlignment(tableHeader.getCell(0), "班级");
WordUtil.setCellTextAndAlignment(tableHeader.addNewTableCell(), "学生");
// 遍历班级列表
for (StudentClass studentClass : list) {
// 获取班级名称
String primaryObjective = studentClass.getClassName();
// 获取班级下的学生列表
List<Student> students = studentClass.getStudents();
// 对于每个一级目标,遍历其所有二级目标
for (Student student : students) {
XWPFTableRow row = table.createRow(); //创建新行
row.getCell(0).setText(primaryObjective); // 班级名称
row.getCell(1).setText(student.getStudentName()); // 设置学生姓 名
}
}
WordUtil.mergeVerticalCells(table, 0); //合并第一列(班级列)
WordUtil.tableStyle(table); //设置表格样式
}
public void setImage(XWPFDocument doc) throws IOException, InvalidFormatException {
WordUtil.addContentParagraphTitle(doc, "三、图片"); // 一级标题
String imagePath = "img.png";
// 获取图片输入流
InputStream pic = new FileInputStream(imagePath);
// 设置图片宽度为4厘米,高度为3厘米
int widthCm = 4;
int heightCm = 3;
int widthEMU = Math.round(widthCm * 360000); // 将厘米转换为EMU
int heightEMU = Math.round(heightCm * 360000);
XWPFParagraph paragraph = doc.createParagraph();
XWPFRun run = paragraph.createRun();
run.addPicture(pic, Document.PICTURE_TYPE_PNG, imagePath, widthEMU, heightEMU);
// 关闭输入流
pic.close();
}
}
接下来直接运行就可以了
这里是运行结果