最近做了一个使用Java生成统计分析报告word文档的功能,有提前制作好的word文档,其中共包含了普通文本变量,普通表格,动态表格、统计图表(柱状图、饼状图、折线图等),在此记录下POI和freemarker两种方法。

  • POI:

POI是使用.docx文件作为模板的,直接将word文档中需要替换的数据改为${变量名},然后通过加载模板并解析为document对象来处理,也可以直接创建一个空的dcoument对象,document通常通过段落(XWPFParagraph)、表格(XWPFTable)、和图片(XWPFPictureData)来解析整个文档。(注意:这里的段落是当格式发生变化时就是一个新的段落,而不是文档中文本的段落)

  1. 一个word文档包含多个段落,一个段落包含多个Runs,一个Runs包含多个Run,Run是文档的最小单位

获取所有段落:List<XWPFParagraph> paragraphs = document.getParagraphs();

获取一个段落中的所有Runs:List<XWPFRun> runs = paragraph.get(0).getRuns();

获取一个Runs中的一个Run:XWPFRun run = runs.get(index);

  1. 一个word文档可包含多个表格,每个表格可以包含多行,每行可以包含多列(格)。注意:每一个单元格又相当于一个完整的文档,包含了XWPFParagraph、XWPFRun

获取所有表格:List<XWPFTable> xwpfTables = doc.getTables();

获取一个表格中的所有行:List<XWPFTableRow> xwpfTableRows = xwpfTable.getRows();

获取一行中的所有列:List<XWPFTableCell> xwpfTableCells = xwpfTableRow.getTableCells();

获取一格里的内容:List<XWPFParagraph> paragraphs = xwpfTableCell.getParagraphs();

  1. 图片是直接获取到一个图片列表xwpfDocument.getAllPictures();

首先解释下XWPFParagraph,普通文本中一个段落就是单行形式,而在表格中一个单元格里也可能会有多个段落XWPFParagraph,多个XWPFRun,所以替换变量值的时候要注意,有时候会出现替换之后,原来的内容还有一部分没替换完。这个多试几次也就会发现了。其次就是POI是要解析整个word文档的,然后将之替换为实际值,因此会有大量编码。但是这样也会对文档内容、格式调整有较好的支持。

  • freemarker

freemarker是以.ftl文件为模板,是先将.docx进行变量替换${变量名},然后将.docx文件另存为xml文件,再改为.xml后缀改为.ftl,就得到了.ftl的模板文件,然后使用FreeMarker引擎将数据填充到模板,来动态渲染生成输出文本。(方法大家自行搜索下,很多的案例)

制作模板过程:

  1. 将 word 中需要填充的数据用占位符${变量名}替换。

  1. 将该 word 另存为 .xml 的格式,并检查看格式是否有误(重点:打开xml文件,查看占位符${变量名}是否完整,如果被分割开了,就复制完整,然后删掉多余的标签)。

  1. 将后缀.xml改成.ftl后,模板就制作完成了。

其中对于表格数据需要单独做处理,可以参考教程https://blog.csdn.net/weixin_45103378/article/details/118395284。

POI实现方式:主要就是要解析出所有的变量,然后进行替换,以下为部分代码片段

public class POIWordUtil {
    /**
     * 替换段落文本
     * @param document docx解析对象
     * @param textMap  需要替换的信息集合
     */
    public static void changeText(XWPFDocument document, Map<String, Object> textMap, Map<String, Object> chartMap) throws IOException {
        //获取段落集合
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        for (XWPFParagraph paragraph : paragraphs) {
            //判断此段落时候需要进行替换
            String text = paragraph.getText();
            if (checkText(text)) {
                List<XWPFRun> runs = paragraph.getRuns();
                for (XWPFRun run : runs) {
                    //判断是否包含$
                    if (!checkText(run.getText(0))) {
                        continue;
                    }

                    //判断是否为chart并替换(这里也是将图表作为变量,然后使用jfreechart生成的图表替换掉)
                    if (isContainsChart(run)) {
                        handleChart(run, chartMap);
                        continue;
                    }

                    //替换文本变量
                    Object obj = matchValue(run.getText(0), textMap);
                    if (obj instanceof String) {
                        run.setText((String) obj, 0);
                    }
                }
            }
        }
    }

    public static void handleTable(XWPFDocument xwpfDocument, Map<String, Object> tableMap) {
        // 获取文档中所有的表格
        List<XWPFTable> tables = xwpfDocument.getTables();
        for (int i = 0; i < tables.size(); i++) {
            XWPFTable table = tables.get(i);
            List<XWPFTableRow> rows = table.getRows();
            // 校验是否需要替换变量
            if (checkText(table.getText())) {
                for (XWPFTableRow row : rows) {
                    List<XWPFTableCell> tableCells = row.getTableCells();
                    for (XWPFTableCell tableCell : tableCells) {
                        if (checkText(tableCell.getText())) {
                            List<XWPFParagraph> paragraphs = tableCell.getParagraphs();
                            for (XWPFParagraph paragraph : paragraphs) {
                                List<XWPFRun> runs = paragraph.getRuns();
                                for (XWPFRun run : runs) {
                                    run.setText(matchValue(tableCell.getText(), tableMap), 0);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * @param text ${...} 带${}的变量text
     * @param map  存储需要替换的数据
     * @return 需要替换的变量值
     * @Description 有${}的值匹配出替换的数据,没有${}就返回原来的数据
     */
    public static String matchValue(String text, Map<String, Object> map) {
        int n = text.length()-text.replaceAll("\\$", "").length();
        for (int i = 0; i < n; i++) {
            String key = text.substring(text.indexOf("$"), text.indexOf("}")+1);
            String key1 = String.valueOf(map.get(key.substring(2, key.indexOf("}"))));
            text = text.replace(key, StringUtils.isEmpty(key1) ? "--" : key1);
        }
        return text;
    }

    public static void handleChart(XWPFRun run, Map<String, Object> chartMap) throws IOException {
        //获取文本的值
        String text = run.getText(0);
        String value = text.substring(text.indexOf("$")+2, text.indexOf("}"));
        Map<String, Object> chartData = (Map<String, Object>) chartMap.get(value);
        
        // 图表数据为空,不生成图表
        if (null == chartData || CollectionUtils.isEmpty(chartData)) {
            run.setText("暂无数据", 0);
            return;
        }
        // 数据全部为0,则生成图表
        if (isAllZero(chartData)) {
            run.setText("暂无数据", 0);
            return;
        }

        String pictureUrl = "";
        InputStream inputStream = null;
        try {
                // 将此处变量设置为空
                run.setText("", 0);
                // 换行
                run.addBreak();
                if (text.indexOf("VerticalBarChart") != -1) {
                    // 纵向柱状图
                    // 使用jfreechart生成图表,并保存为图片,返回图片的path
                    pictureUrl = JfreeUtil.createBarChart("", chartData, "", "", PlotOrientation.VERTICAL, 700 ,400);
                    inputStream=new FileInputStream(pictureUrl);
                    // 添加图片
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, "", Units.toEMU(400), Units.toEMU(250));
                } else if (text.indexOf("HorizontalBarChart") != -1) {
                    // 横向柱状图
                    pictureUrl = JfreeUtil.createBarChart("", chartData, "", "", PlotOrientation.HORIZONTAL, 700 ,400);
                    inputStream=new FileInputStream(pictureUrl);
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, "", Units.toEMU(400), Units.toEMU(250));
                } else if (text.indexOf("PieChart") != -1) {
                    // 饼图
                    Map<String, Double> doubleMap = new LinkedHashMap<>();
                    chartData.forEach((k,v) -> doubleMap.put(k, Double.valueOf(v.toString())));
                    pictureUrl = JfreeUtil.createPieChart("", doubleMap, 600, 500);
                    inputStream=new FileInputStream(pictureUrl);
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, "", Units.toEMU(360), Units.toEMU(300));
                } else if (text.indexOf("LineChart") != -1) {
                    // 折线图
                    pictureUrl = JfreeUtil.createLineChart("", chartData, "", "", PlotOrientation.VERTICAL, 800 ,400);
                    inputStream=new FileInputStream(pictureUrl);
                    run.addPicture(inputStream, XWPFDocument.PICTURE_TYPE_PNG, "", Units.toEMU(400), Units.toEMU(200));
                }
        } catch (Exception e) {
            log.error("handle chart fail", e);
            throw new ConsciousException("创建图表失败!");
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }

            File pictureFile = new File(pictureUrl);
            if(pictureFile.exists()){
                pictureFile.delete();
            }
        }
    }
}
/**
* 创建图集图表
*/
public class JfreeUtil {

    private static final Font FONT = new Font("宋体", Font.PLAIN, 12);

    public static String createPieChart(String title, Map<String, Double> datas, int width, int height) throws IOException {
        //根据jfree生成一个本地饼状图
        DefaultPieDataset pds = new DefaultPieDataset();
        datas.forEach(pds::setValue);
        //图标标题、数据集合、是否显示图例标识、是否显示tooltips、是否支持超链接
        JFreeChart chart = ChartFactory.createPieChart(title, pds, true, false, false);
        chart.getTitle().setFont(FONT);
        chart.getLegend().setItemFont(FONT);
        //设置抗锯齿
        chart.setTextAntiAlias(false);

        PiePlot plot = (PiePlot) chart.getPlot();
        plot.setStartAngle(90);
        plot.setNoDataMessage("暂无数据");
        plot.setNoDataMessagePaint(Color.blue); // 设置无数据时的信息显示颜色

        //忽略无值的分类
        plot.setIgnoreNullValues(true);
        plot.setIgnoreZeroValues(true);
        plot.setBackgroundAlpha(0f);
        //设置标签阴影颜色
        plot.setShadowPaint(new Color(255, 255, 255));

        //设置标签是否显示在饼块内部,默认时在外部
        plot.setSimpleLabels(true);

        chart.getLegend().setHorizontalAlignment(HorizontalAlignment.CENTER);//设置水平对齐 左对齐;
        chart.getLegend().setMargin(0, 0, 0, 0);//参数是:上,左,下,右. 设置饼图的位置
        chart.getLegend().setPadding(0, 0, 20, 0);// 设置饼图下文字的位置
        chart.getLegend().setFrame(new BlockBorder(0, 0, 0, 0));// 设置饼图下文字边框的位置
        // 图片中显示百分比:自定义方式,{0} 表示选项, {1} 表示数值, {2} 表示所占比例 ,小数点后两位
        NumberFormat percentInstance = NumberFormat.getPercentInstance();
        percentInstance.setRoundingMode(RoundingMode.HALF_UP);
        percentInstance.setMaximumFractionDigits(0);
        plot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0},{2}", NumberFormat.getNumberInstance(), percentInstance));

        return createFile(chart, width, height);
    }

    public static String createBarChart(String title, Map<String, Object> datas, String type, String units, PlotOrientation orientation, int width, int height) throws IOException {
        //数据集
        DefaultCategoryDataset ds = new DefaultCategoryDataset();
        Iterator<Map.Entry<String, Object>> iterator = datas.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Object> entry = iterator.next();
            ds.setValue(NumberUtil.parseDouble(String.valueOf(entry.getValue())), "工单数量", StringUtils.defaultString(entry.getKey(), ""));
        }

        //创建柱状图,柱状图分水平显示和垂直显示两种
        JFreeChart chart = ChartFactory.createBarChart(title, type, units, ds, orientation, false, false, false);

        //设置文本抗锯齿,防止乱码
        chart.setTextAntiAlias(false);
        //得到绘图区
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        plot.setNoDataMessage("no data");
        //设置柱的透明度
        plot.setForegroundAlpha(1.0f);
        plot.setOutlineVisible(false);

        //获取X轴的对象
        CategoryAxis categoryAxis = plot.getDomainAxis();
        //坐标轴标尺值是否显示
        categoryAxis.setTickLabelsVisible(true);
        //坐标轴标尺是否显示
        categoryAxis.setTickMarksVisible(false);
        categoryAxis.setTickLabelFont(FONT);
        categoryAxis.setTickLabelPaint(Color.BLACK);
        categoryAxis.setLabelFont(FONT);// X轴标题
        //categoryAxis.setCategoryLabelPositionOffset(2);//图表横轴与标签的距离(10像素)

        //获取Y轴对象
        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setTickLabelsVisible(true);
        valueAxis.setTickMarksVisible(false);
        valueAxis.setUpperMargin(0.15);//设置最高的一个柱与图片顶端的距离(最高柱的20%)
        valueAxis.setLowerMargin(0d);
        valueAxis.setTickLabelFont(FONT);//Y轴数值
        valueAxis.setLabelPaint(Color.BLACK);//字体颜色
        valueAxis.setLabelFont(FONT);//Y轴标题

        NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
        //设置Y轴刻度为整数
        numberAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

        //设置网格横线颜色
        plot.setRangeGridlinePaint(Color.gray);
        plot.setRangeGridlinesVisible(true);
        //图片背景色
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        // 设置原点xy轴相交,柱子从横轴开始,否则会有间隙
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
        //设置网格横线大小
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        //设置柱状图柱子相关
        CategoryPlot categoryPlot = chart.getCategoryPlot();
        BarRenderer rendererBar = (BarRenderer) categoryPlot.getRenderer();

        //组内柱子间隔为组宽的10%,调整柱子宽度
        rendererBar.setItemMargin(0.6);
        rendererBar.setMaximumBarWidth(0.07);

        rendererBar.setDrawBarOutline(true);
        rendererBar.setSeriesOutlinePaint(0, Color.decode("#4F97D5"));
        //设置柱的颜色#5B9BE6
        rendererBar.setSeriesPaint(0, Color.decode("#4F97D5"));

        //设置柱子上显示值
        rendererBar.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        rendererBar.setDefaultItemLabelFont(FONT);
        rendererBar.setDefaultItemLabelsVisible(true);
        rendererBar.setDefaultItemLabelPaint(Color.BLACK);

        return createFile(chart, width, height);

    }

    public static String createLineChart(String title, Map<String, Object> datas, String type, String unit, PlotOrientation orientation, int width, int hight) throws IOException {
        DefaultCategoryDataset ds = new DefaultCategoryDataset();
        Iterator iterator = datas.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            ds.setValue(NumberUtil.parseDouble(String.valueOf(entry.getValue())), "工单数量", entry.getKey().toString());
        }

        //创建折线图,折线图分水平显示和垂直显示两种
        JFreeChart chart = ChartFactory.createLineChart(title, type, unit, ds, orientation, false, true, true);
        //设置文本抗锯齿,防止乱码
        chart.setTextAntiAlias(false);
        //chart.setBorderVisible(true);

        //得到绘图区
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        //设置横轴标签项字体
        CategoryAxis categoryAxis = plot.getDomainAxis();
        categoryAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45);
        categoryAxis.setTickMarksVisible(false);
        categoryAxis.setTickLabelsVisible(true);
        categoryAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
        categoryAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));

        ValueAxis valueAxis = plot.getRangeAxis();
        valueAxis.setTickMarksVisible(false);
        valueAxis.setTickLabelsVisible(true);
        valueAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
        valueAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));

        NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
        //设置Y轴刻度跨度
        numberAxis.setUpperMargin(0.15);
        numberAxis.setLowerMargin(0);
        numberAxis.setAutoRangeMinimumSize(5);

        // 设置背景透明度
        plot.setBackgroundAlpha(0.1f);
        plot.setForegroundAlpha(1.0f);
        // 设置网格横线颜色
        plot.setRangeGridlinePaint(Color.gray);
        // 设置网格横线大小
        plot.setDomainGridlineStroke(new BasicStroke(0.5F));
        plot.setRangeGridlineStroke(new BasicStroke(0.5F));
        plot.setBackgroundPaint(Color.white);
        plot.setOutlineVisible(false);

        // 设置原点xy轴相交,y轴为0时,点在横坐标上,否则不在横坐标上
        plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));

        // 生成折线图上的数字
        //绘图区域(红色矩形框的部分)
        LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
        renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        //设置图表上的数字可见
        renderer.setDefaultItemLabelsVisible(true);
        //设置图表上的数字字体
        renderer.setDefaultItemLabelFont(new Font("宋体", Font.PLAIN, 12));

        // 设置线条是否被显示填充颜色
        renderer.setUseFillPaint(true);
        renderer.setSeriesStroke(0, new BasicStroke(4.0f));
        renderer.setSeriesPaint(0, Color.decode("#4472C4"));

        return createFile(chart, width, hight);
    }

    public static String createFile(JFreeChart chart, int width, int hight) throws IOException {
        File templateFile = File.createTempFile("jfreetemp", ".png");
        String filePath = templateFile.getParent() + File.separator + templateFile.getName();
        try {
            if (templateFile.exists()) {
                templateFile.delete();
            }
            ChartUtils.saveChartAsPNG(templateFile, chart, width, hight);
        } catch (IOException e) {
            log.error("create chart file fail.", e);
            throw new ConsciousException("创建图表文件失败!");
        }

        return filePath;
    }


}

图表要注意的设置:(个人觉得是比较有用的)

//设置标签是否显示在饼块内部,默认时在外部

plot.setSimpleLabels(true);

//组内柱子间隔为组宽的10%,调整柱子宽度

rendererBar.setItemMargin(0.6);

rendererBar.setMaximumBarWidth(0.07);

// 设置原点xy轴相交,零点与坐标轴重合,

plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));

freemarker实现方式,首先获取到模板Template

public class FreeMarkerUtils {

    public static Template getTemplate(String templateName) throws IOException {
        // 设置FreeMarker的版本和编码格式
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_30);
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassicCompatible(true);
        // 设置FreeMarker生成Word文档所需要的模板的路径
        // 此处把模版文件都放在resources下的 templates 中
        configuration.setClassForTemplateLoading(FreeMarkerUtils.class, "/templates");
        configuration.setTemplateLoader(new ClassTemplateLoader(FreeMarkerUtils.class, "/templates"));
        // 设置FreeMarker生成Word文档所需要的模板
        return configuration.getTemplate(templateName, "utf-8");
    }
}

然后填充数据即可

public static void main(String[] args) throws IOException {
        String fileName = "testdemo.docx";
        Map<String, String> dataMap = new HashMap<>();

        Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(fileName)), StandardCharsets.UTF_8));
        Template template = FreeMarkerUtils.getTemplate("testdemo.ftl");
        // FreeMarker使用Word模板和数据生成Word文档
        try {
            template.process(dataMap, out);
        } catch (TemplateException e) {
            throw new RuntimeException(e);
        }
        out.flush();
        out.close();
    }

遗留问题:关于动态表格的处理,如何根据数据长度来动态调整表格的长度。貌似不能删除单元格和某一列,remove方法没用,只能置空。增加行、列后,单元格内的文本格式调整不生效,例如左对齐,居中等。欢迎评论区解答!

以上就是关于生成word的方法,大致描述了下每种的方法的思路,然后就是在每个过程中有一些注意点,还是比较重要的。也是第一次使用,有不对的地方希望大家多加指教,评论区指出,谢谢!

更多推荐