Java 功能实现 - Apache POI 4.1.1 实现Word导出

文章目录

  • 〇、前言
  • 一、实现Word功能技术点分析
  • 二、相关代码
    • 1. jar包导入
    • 2. 整体流程
      • (1) 操作前后图
      • (2) 代码
    • 3. 涉及到的对象解释
    • 4. 段落相关操作
    • 5. 文本操作
  • 三、写在最后

〇、前言

  • 关于Word的导出我是第一次实现,实现这个导出的需求整整花了一周,调来调去耗费太多时间。搞了这么久,除了需求实现的一些难点外,感觉学习方法也有问题。
  • 我说一下自己对这种初级接触的功能点的实现思路, 各位大佬若看到本篇博客,希望能指导指导哈哈!
    • 1.查阅可以实现Word功能的技术点
    • 2.分析个技术点的优缺点
    • 3.综合项目情况,确定采用技术
    • 4.下载Api文档、参考大佬博客

一、实现Word功能技术点分析

  • 根据自己的查阅,主要有如下几种,
技术点 优点 缺点
Jacob
Java2word
FreeMarker
PageOffice
Apache POI
  • 通过查阅发现博客中使用 Apache POI 的人数偏多些,其也能满足自己的需求,项目中刚好也有 Apache POI 4.1 的依赖,所以就选择了它,
  • 此外hutool 也有提供一个工具类:Word07Writer,链接点这

二、相关代码

  • Apache POI 4.1的Api文档链接
  • 因为涉及的功能逻辑不一样,目前就把 整体流程 和 一些通用方法做个记录,很多都是站在巨人的肩膀上。

1. jar包导入

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>4.1.1</version>
        </dependency>

2. 整体流程

(1) 操作前后图

  • 操作前
    Java 功能实现 - Apache POI 4.1.1 实现Word导出_第1张图片

  • 操作后
    Java 功能实现 - Apache POI 4.1.1 实现Word导出_第2张图片

(2) 代码

    public static void exportWord() {
        InputStream inputDocxTemplate = null;
        OutputStream outputStream = null;
        XWPFDocument document = null;

        try {
            log.info("1.导入word模板");
            inputDocxTemplate = EverydayLearnApplication.class.getResourceAsStream("/template/WordExportTemplate.docx");
            document = new XWPFDocument(inputDocxTemplate);

            log.info("2.操作逻辑");
            log.info("2.1 替换 文档中的 文本 占位符");
            // 构建替换 Map
            Map<String, String> replaceTextMap = new HashMap<>();
            replaceTextMap.put("${name}", "任初心");
            replaceTextMap.put("${age}", "18");
            replaceTextMap.put("${sex}", "男");
            replaceTextMap.put("${hobby}", "跑步");
            replaceTextMap.put("${contactWay}", "12345678910");
            // 替换文本占位符
            doReplaceText(document, replaceTextMap);

            log.info("2.2 操作已有表格");
            // 获取表格
            XWPFTable oneTable = document.getTableArray(0);
            // 构建数据, 内层 List 每一个值表示一个单元格的数据
            List<List<String>> rowDataList = new ArrayList<>();
            List<String> oneRow = new ArrayList<>();
            oneRow.add("框架");
            oneRow.add("SpringCloud");
            oneRow.add("熟悉");
            List<String> twoRow = new ArrayList<>();
            twoRow.add("数据库");
            twoRow.add("MongoDB");
            twoRow.add("熟悉");
            rowDataList.add(oneRow);
            rowDataList.add(twoRow);
            // 插入数据
            insertDataToTable(oneTable, rowDataList);

            log.info("2.3 替换 表格 占位符");
            // 第一行(表头)
            List<String> titleList = Arrays.asList("技术分类", "技术点", "熟悉程度");
            // 内容行
            List<List<String>> tableDataList = new ArrayList<>();
            // 用上面构建的数据,就不再重新建了
            tableDataList.add(oneRow);
            tableDataList.add(twoRow);
            // 替换表格占位符
            doReplaceTable(document, "${table}", titleList, tableDataList);

            log.info("2.4 替换 图片占位符");

            URL resource = EverydayLearnApplication.class.getClassLoader().getResource("template/picture/good.png");
            String picturePath = resource.getPath();
            String decodePicturePath = URLDecoder.decode(picturePath, "UTF-8");
            log.info("picturePath:{}", decodePicturePath);
            // 替换图片占位符
            doReplacePicture(document, "${picture}", decodePicturePath);

            log.info("3.word 导出");
            ApplicationHome ah = new ApplicationHome();
            File fileHone = ah.getDir();
            String filePath = fileHone.getParentFile().getAbsolutePath() + File.separator + "export" + File.separator;
            String fileName = "演示模板.docx";

            FileUtil.mkdirs(filePath);
            File file = new File(filePath + fileName);
            outputStream = new FileOutputStream(file);
            document.write(outputStream);
            outputStream.flush();
            outputStream.close();
            document.close();
            inputDocxTemplate.close();
        } catch (IOException e) {
            log.error("word导出异常:{}", e.toString());
        } finally {
            closeStream(outputStream);
            closeStream(inputDocxTemplate);

            if (null != document) {
                try {
                    document.close();
                } catch (IOException e) {
                    log.error("word文档关流失败:{}", e.toString());
                }
            }

        }
    }

    /**
     * 插入图片, 替换文档中的图片占位符
     *
     * @param document    word模板对象
     * @param key         图片占位符
     * @param picturePath 图片所在路径
     */
    private static void doReplacePicture(XWPFDocument document, String key, String picturePath) {
        FileInputStream inputStream = null;

        try {
            inputStream  = new FileInputStream(new File(picturePath));

            List<XWPFParagraph> paragraphList = document.getParagraphs();
            quit: for (int i = 0; i < paragraphList.size(); i++) {

                List<XWPFRun> runList = paragraphList.get(i).getRuns();
                for (int j = 0; j < runList.size(); j++) {

                    String runText = runList.get(j).getText(0);
                    if (StrUtil.isNotBlank(runText) && runList.get(j).getText(0).indexOf(key) != -1) {
                        // 获取光标
                        XmlCursor cursor = paragraphList.get(i).getCTP().newCursor();
                        // 插入新的段落
                        XWPFParagraph newParagraph = document.insertNewParagraph(cursor);
                        newParagraph.setAlignment(ParagraphAlignment.CENTER);
                        XWPFRun run = newParagraph.createRun();
                        run.addPicture(inputStream, Document.PICTURE_TYPE_PNG, "good.png", Units.toEMU(300), Units.toEMU(160));

                        // 删除占位符
                        document.removeBodyElement(document.getPosOfParagraph(paragraphList.get(i+1)));
                        break quit;
                    }
                }

            }
            inputStream.close();
        } catch (Exception e) {
            log.error("替换图片异常:{}", e.toString());
        } finally {
            closeStream(inputStream);
        }

    }

    /**
     * 插入的新表格, 替换文档中的表格占位符
     *
     * @param document      word模板对象
     * @param key           表格占位符
     * @param titleList     第一行(表头)
     * @param tableDataList 表格数据内容
     */
    private static void doReplaceTable(XWPFDocument document, String key, List<String> titleList, List<List<String>> tableDataList) {
        List<XWPFParagraph> paragraphList = document.getParagraphs();

        for (int i = 0; i < paragraphList.size(); i++) {

            List<XWPFRun> runList = paragraphList.get(i).getRuns();
            for (int j = 0; j < runList.size(); j++) {

                String runText = runList.get(j).getText(0);
                if (StrUtil.isNotBlank(runText) && runList.get(j).getText(0).indexOf(key) != -1) {
                    // 获取光标
                    XmlCursor cursor = paragraphList.get(i).getCTP().newCursor();
                    // 插入新的表格
                    XWPFTable newTable = document.insertNewTbl(cursor);
                    // 设置表格样式
                    setTableStyle(newTable);
                    // 创建表格的第一行(表格创建后,默认有一个单元格)
                    XWPFTableCell titleOneCell = newTable.getRow(0).getCell(0);
                    titleOneCell.setWidth("auto");
                    setCellTitleStyle(titleOneCell, titleList.get(0));
                    for (int p = 1; p < titleList.size() ; p++) {
                        XWPFTableCell titleCell = newTable.getRow(0).createCell();
                        titleCell.setWidth("auto");
                        setCellTitleStyle(titleCell, titleList.get(p));
                    }
                    // 根据数据量创建表格其他行
                    for (List<String> rowDataList : tableDataList) {
                        XWPFTableRow row = newTable.createRow();

                        for (int p = 0; p < titleList.size(); p++) {
                            XWPFTableCell cell = row.getCell(p);
                            cell.setText(rowDataList.get(p));
                            setCellDataStyle(cell);
                        }
                    }
                    // 删除占位符
                    paragraphList.get(i).removeRun(0);
                }
            }

        }
    }

    /**
     * 设置表格样式
     *
     * @param table 表格对象
     */
    private static void setTableStyle(XWPFTable table) {
        CTTblPr tblPr = table.getCTTbl().getTblPr();
        tblPr.getTblW().setType(STTblWidth.DXA);
        tblPr.getTblW().setW(new BigInteger("8000"));
        tblPr.addNewJc().setVal(STJc.CENTER);
    }


    /**
     * 设置表格第一行(表头)单元格的样式
     *
     * @param titleCell 表头单元格
     * @param text      单元格文本内容
     */
    private static void setCellTitleStyle(XWPFTableCell titleCell, String text) {
        XWPFParagraph xwpfParagraph = titleCell.addParagraph();
        xwpfParagraph.setAlignment(ParagraphAlignment.CENTER);
        XWPFRun run = xwpfParagraph.createRun();
        run.setText(text);
        run.setFontSize(14);
        run.setBold(true);
        titleCell.removeParagraph(0);
    }

    /**
     * 设置单元格 文本内容 样式:居中
     *
     * @param cell 单元格
     */
    public static void setCellDataStyle(XWPFTableCell cell) {
        // 设置表格样式一致,默认是左对齐
        CTTc cttc = cell.getCTTc();
        CTTcPr ctPr = cttc.addNewTcPr();
        ctPr.addNewVAlign().setVal(STVerticalJc.CENTER);
        cttc.getPList().get(0).addNewPPr().addNewJc().setVal(STJc.CENTER);
    }

    /**
     * 往表格中插入数据
     *
     * @param table   表格对象
     * @param rowList 要插入的数据
     */
    private static void insertDataToTable(XWPFTable table, List<List<String>> rowList) {
        // 根据传入数据数量 创建行
        for (List<String> row : rowList) {
            table.createRow();
        }

        List<XWPFTableRow> rows = table.getRows();
        // 第一行为标题,从第二行开始操作
        for (int i = 1; i < rows.size(); i++) {

            List<XWPFTableCell> cells = rows.get(i).getTableCells();
            for (int j = 0; j < cells.size(); j++) {
                XWPFTableCell cell = cells.get(j);
                cell.setText(rowList.get(i - 1).get(j));
                // 设置样式为居中
                setCellDataStyle(cell);
            }
        }
    }

    /**
     * 替换 word 模板的占位符
     *
     * @param document       Api文档模板
     * @param replaceTextMap 要替换的文本信息 Map<占位符, 要替换的值>
     */
    private static void doReplaceText(XWPFDocument document, Map<String, String> replaceTextMap) {
        List<XWPFParagraph> paragraphs = document.getParagraphs();

        for (XWPFParagraph paragraph : paragraphs) {

            String paragraphText = paragraph.getText();
            if (whetherNeedReplace(paragraphText)) {

                List<XWPFRun> runs = paragraph.getRuns();
                for (XWPFRun run : runs) {

                    String runText = run.getText(0);
                    for (Map.Entry<String, String> entry : replaceTextMap.entrySet()) {
                        String key = entry.getKey();
                        if (runText.indexOf(key) != -1) {
                            String value = entry.getValue();
                            String replaceValue = runText.replace(key, value);
                            run.setText(replaceValue, 0);
                        }
                    }
                }
            }
        }
    }

    /**
     * 判断段落的文本是否需要替换
     *
     * @param paragraphText 段落文本
     * @return true 需要替换, false 不需要替换
     */
    private static boolean whetherNeedReplace(String paragraphText) {
        boolean needReplace = false;
        if (paragraphText.indexOf("$") != -1) {
            needReplace = true;
        }
        return needReplace;
    }

    /**
     * 关闭输入流
     *
     * @param inputStream 输入流对象
     */
    public static void closeStream(InputStream inputStream) {
        if (null != inputStream) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("输出流关闭失败:{}", e.toString());
            }
        }
    }

    /**
     * 关闭输出流 对象
     *
     * @param outputStream
     */
    public static void closeStream(OutputStream outputStream) {
        if (null != outputStream) {
            try {
                outputStream.close();
            } catch (IOException e) {
                log.error("输出流关闭失败:{}", e.toString());
            }
        }
    }
  • 注意点:Word模板的占位符,如“${text}”,可能会被切割成“${”、“text”、“}”,导致替换失败,出现这种情况,有两种解决方案:
    1. 将占位符复制到随便一款文本软件中,再重新复制回word模板。
    2. 将word模板保存为 xml 格式,打开 xml 将这三部分替换成一部分,而后再保存为 .docx 格式

    具体操作:
    1.打开保存后的.xml 文件,复制内容到在线解析网站(这边附了一个)
    2.搜索替换成一个整体,如下所示:
    Java 功能实现 - Apache POI 4.1.1 实现Word导出_第3张图片
    3.保存修改,将".xml"文件改为“.docx"文件

3. 涉及到的对象解释

// 表示一个docx文档,其可以用来读docx文档,也可以用来写docx文档
XWPFDocument
// 表示一个段落,由多个XWPFRun组成
XWPFParagraph
// 表示一段文本,一个段落可由多个 XWPFRun 组成
XWPFRun
// 表示一个表格
XWPFTable
// 表示表格的一行
XWPFTableRow
// 表示表格的一个单元格
XWPFTableCell
// 表示一张图片
XWPFPicture
// 表示超链接
XWPFHyperlink
// 表示docx文件中的图表
XWPFChar

4. 段落相关操作

  • 以下操作部分摘自其他前辈博客
    • 前辈博客地址
/**
 * 设置段落内文本上下对齐方式【段落-中文板式-文本对齐方式】
 * @param textAlignment  {@link STTextAlignment#CENTER}
 */
public static void setTextAlignment(XWPFParagraph paragraph , STTextAlignment.Enum textAlignment){
    CTP ctp = paragraph.getCTP();
    CTPPr ctpPr = ctp.isSetPPr() ? ctp.getPPr() : ctp.addNewPPr();
    CTTextAlignment ctTextAlignment = CTTextAlignment.Factory.newInstance();
    ctTextAlignment.setVal(textAlignment);
    ctpPr.setTextAlignment(ctTextAlignment);
}

/**
 * 调整段落间距【对应word段落设置-间距设置】
 * @param before        段前  磅
 * @param after         段后  磅
 * @param multiple      几倍行距
 */
public static void setParagraphSpacing(XWPFParagraph paragraph , float before , float after , float multiple) {
    CTPPr ppr = paragraph.getCTP().getPPr();
    if (ppr == null) {
        ppr = paragraph.getCTP().addNewPPr();
    }
    CTSpacing spacing = ppr.isSetSpacing()? ppr.getSpacing() : ppr.addNewSpacing();
    spacing.setBefore(Math.round(before * 20));
    spacing.setAfter(Math.round(after * 20));
    spacing.setLine(Math.round(240 * multiple));
    spacing.setLineRule(STLineSpacingRule.AUTO);
}

/**
 * 设置段落缩进
 * @param left   左缩进值 磅
 * @param right  右缩进值 磅
 */
public static void setParagraphInd(XWPFParagraph paragraph , float left , float right){
    CTPPr ppr = paragraph.getCTP().getPPr();
    if (ppr == null) {
        ppr = paragraph.getCTP().addNewPPr();
    }
    CTInd ctInd = ppr.isSetInd() ? ppr.getInd() : ppr.addNewInd();
    if(left >= 0){
        ctInd.setLeft(Math.round(left * 20));
    }
    if(right >= 0){
        ctInd.setRight(Math.round(right * 20));
    }
}

5. 文本操作

/**
 * 设置文本填充色
 */
public static void setFillColor(XWPFRun run , String color){
    CTR ctr = run.getCTR();
    CTRPr rPr = ctr.isSetRPr() ? ctr.getRPr() : ctr.addNewRPr();
    CTShd ctShd = rPr.sizeOfShdArray() > 0 ? rPr.getShdArray(0) : rPr.addNewShd();
    ctShd.setFill(color);
}

/**
 * 设置文本 Run位置【对应word字体 - 高级 - 字符间距】
 * @param positionValue    【上下位置调整正负】 单位 磅
 * @param spacingValue     【字符间距调整 宽窄-正负】单位 磅
 */
public static void setPositionRun(XWPFRun run , float positionValue , float spacingValue){
    CTRPr rPr = run.getCTR().getRPr();
    if(rPr == null){
        rPr = run.getCTR().addNewRPr();
    }
    CTSignedHpsMeasure position = rPr.sizeOfPositionArray() > 0 ? rPr.getPositionArray(0) : null;
    if(position == null){
        rPr.addNewPosition().setVal(positionValue * 2);
    }else{
        position.setVal(positionValue * 2);
    }
    CTSignedTwipsMeasure spacing = rPr.sizeOfSpacingArray() > 0 ? rPr.getSpacingArray(0) : null;
    if(spacing == null){
        rPr.addNewSpacing().setVal(spacingValue * 20);
    }else{
        spacing.setVal(spacingValue * 20);
    }
}

三、写在最后

  • 实现这个功能点,学习了挺多前辈写的博客,想着能够减少其他人的搜索时间,后面的功能操作摘录了一些前辈写的,但有些自己还未经检验,所以本篇博客后续还会进行补充和完善,尽量达到完整。
  • 各位前辈若看到博客中有什么问题,请多多指教。

你可能感兴趣的:(Java,功能实现,word,java,apache)