天天看点

POI生成word文档,包括标题,段落,表格,统计图(非图片格式)

Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。
           

POI生成excel是比较常用的技术之一,但是用来生成word相对来说比较少,今天演示一下利用POI生成word文档的整个流程,,当然方法有很多种,这是我感觉比较方便的一种。

需要实现的功能:

  • 在文档中动态生成章节标题、文本段落、表格标题、表格、统计图等等。
  • 给每个章节编号
  • 删除没有替换的占位符

总体思路:

这种方法的本质其实是动态替换,动态替换的意思是在模板中写入很多某种特定格式的占位符(关键词)以及图的样例,如果前端需要生成这个章节的内容,那么,把这个关键词传到后端,再在模板中寻找关键词,有则替换成具体内容,无则不替换。最后把没有替换掉的占位符删除,从而达到动态替换的效果。

具体实现过程如下:

  1. 创建word模板.文档中包含若干个章节,章节中包含章节标题、文本段落、表格标题、表格、统计图的占位符。占位符的内容依据自己的实际情况而定。
  2. 加载模板,判断该章节是否存在,如果存在就替换具体数据。本示例数据都是模拟的,实际情况应该从数据库中查出。
  3. 再次遍历替换后的word文档,把没有替换掉的占位符删除。

示例

1. 创建模板

如下图所示:

POI生成word文档,包括标题,段落,表格,统计图(非图片格式)

2.替换数据

2.1 测试类

import org.apache.poi.xwpf.usermodel.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ResourceUtils;

import java.io.*;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PoiChartsTest {
    @Autowired
    private  PoiPropsConfig config;

    @Autowired
    private PoiUtils poiUtils;

    //预编译正则表达式,加快正则匹配速度
    private  Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);

    @Test
    public void test() throws Exception {
        XWPFDocument document = null;
        InputStream inputStream=null;
        try {
            File file = ResourceUtils.getFile("D://poi.docx");
            inputStream = new FileInputStream(file);
            document = new XWPFDocument(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Iterator<XWPFParagraph> itPara = document.getParagraphsIterator();

        //处理文字
        while (itPara.hasNext()) {
            XWPFParagraph paragraph = itPara.next();
            String paraText = paragraph.getText();
            //如果没有匹配到指定格式的关键词占位符(如${title}格式的)则不进行后续处理
            if (!pattern.matcher(paraText).find()) {
                continue;
            }
            //提取出文档模板占位符中的章节标题
            String keyInParaText = paraText.split("\\$\\{")[1].split("\\}")[0];
            //如果占位符是大标题
            if ("title".equalsIgnoreCase(keyInParaText)) {
                insertTitle(paragraph);
                continue;
            }
            //如果占位符代表文本总描述
            if ("totalDesc".equalsIgnoreCase(keyInParaText)) {
                insertText(paragraph);
                continue;
            }
            //如果占位符代表章节标题
            if (keyInParaText.contains("section") && keyInParaText.contains("Title")) {
            	//获取章节类名
                String name = keyInParaText.substring(0, 8);
                //获取章节类的路径
                String classPath = config.getSection().get(name);
                //通过类路径获取类对象
                BaseSection base = (BaseSection) Class.forName(classPath).newInstance();
                base.replaceSectionTitle(document, paragraph);
                continue;
            }
            //如果占位符代表章节文本描述
            if (keyInParaText.contains("body")) {
                String name = keyInParaText.substring(0, 8);
                BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
                base.replaceBody(paragraph);
                continue;
            }
            //如果占位符代表表名
            if (keyInParaText.contains("tableName")) {
                String name = keyInParaText.substring(0, 8);
                BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
                base.replaceTableName(paragraph);
                continue;
            }
            //如果占位符代表表
            if (keyInParaText.endsWith("table")) {
                String name = keyInParaText.substring(0, 8);
                BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
                base.insertTable(document, paragraph);
                continue;
            }
            //如果占位符代表统计图
            if (keyInParaText.endsWith("chart")) {
                String name = keyInParaText.substring(0, 8);
                paragraph.removeRun(0);
                BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
                base.replaceChart(document, keyInParaText);
                continue;
            }
            //如果占位符代表图名
            if (keyInParaText.contains("chartName")) {
                String name = keyInParaText.substring(0, 8);
                BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
                base.replaceChartName(paragraph);
                continue;
            }
        }

        //再遍历一次文档,把没有替换的占位符段落删除
        List<IBodyElement> elements = document.getBodyElements();
        int indexTable = 0;
        for (int k = 0; k < elements.size(); k++) {
            IBodyElement bodyElement = elements.get(k);
            //所有段落,如果有${}格式的段落便删除该段落
            if (bodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) {
                XWPFParagraph p = (XWPFParagraph) bodyElement;
                String paraText = p.getText();
                boolean flag = false;
                if (pattern.matcher(paraText).find()) {
                    flag = document.removeBodyElement(k);
                    if (flag) {
                        k--;
                    }
                }
            }
            //如果是表格,那么给表格的前一个段落(即表名加上编号,如表1)
            if (bodyElement.getElementType().equals(BodyElementType.TABLE)) {
                indexTable++;
                XWPFParagraph tableTitleParagraph = (XWPFParagraph) elements.get(k - 1);
                StringBuilder tableTitleText = new StringBuilder(tableTitleParagraph.getParagraphText());
                tableTitleText.insert(0, "表" + indexTable + " ");
                poiUtils.setTableOrChartTitle(tableTitleParagraph, tableTitleText.toString());
            }
        }

        //给章节与小节添加序号
        poiUtils.init(document);

        //导出word文档
        FileOutputStream docxFos = new FileOutputStream("D://test1.docx");
        document.write(docxFos);
        docxFos.flush();
        docxFos.close();
        inputStream.close();
    }

    //插入大标题
    public void insertTitle(XWPFParagraph paragraph) {
        String title = "步步升超市报告";
        List<XWPFRun> runs = paragraph.getRuns();
        int runSize = runs.size();
        /**Paragrap中每删除一个run,其所有的run对象就会动态变化,即不能同时遍历和删除*/
        int haveRemoved = 0;
        for (int runIndex = 0; runIndex < runSize; runIndex++) {
            paragraph.removeRun(runIndex - haveRemoved);
            haveRemoved++;
        }
        /**3.插入新的Run即将新的文本插入段落*/
        XWPFRun createRun = paragraph.insertNewRun(0);
        createRun.setText(title);
        XWPFRun separtor = paragraph.insertNewRun(1);
        /**两段之间添加换行*/
        separtor.setText("\r");
        //设置字体大小
        createRun.setFontSize(22);
        //是否加粗
        createRun.setBold(true);
        //设置字体
        createRun.setFontFamily("宋体");
        //设置居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
    }

    //插入文本描述
    private void insertText(XWPFParagraph paragraph) {
        String text = "步步升超市作为零售业的典型代表,它已经在全国迅速发展起来。在2018年上半年取得了不菲的成绩," +
                "创造销售额230亿,本着方便于民,服务于民的宗旨,我们会继续努力。以下是详细信息报告:"
        poiUtils.setTextPro(paragraph, text);
    }
}
           

2.2 章节基类

因为要实现动态生成,所以要用到反射和多态。

import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTChart;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTTitle;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTTx;
import org.openxmlformats.schemas.drawingml.x2006.main.CTTextBody;


import java.util.ArrayList;
import java.util.List;

public abstract class BaseSection {
    PoiUtils poiUtils = new PoiUtils();

    //替换章节标题
    public abstract void replaceSectionTitle(XWPFDocument document, XWPFParagraph paragraph);

    //替换章节内容
    public abstract void replaceBody(XWPFParagraph paragraph);

    //替换章节表名
    public  void replaceTableName(XWPFParagraph paragraph){
        String tableName="水果销售数据";
        poiUtils.setTableOrChartTitle(paragraph,tableName);
    }

    //生成表
    public void insertTable(XWPFDocument document, XWPFParagraph paragraph) {
        /*1.将段落原有文本(原有所有的Run)全部删除*/
        poiUtils.deleteRun(paragraph);
        /*生成表格插入提示游标*/
        XmlCursor cursor = paragraph.getCTP().newCursor();
        // 在指定游标位置插入表格
        XWPFTable table = document.insertNewTbl(cursor);
        //设置表格居中
        table.setTableAlignment(TableRowAlign.CENTER);
        //模拟表格数据
        List<String[]> list = new ArrayList<>();
        list.add(new String[]{"苹果", "100", "6", "600", "10.7"});
        list.add(new String[]{"香蕉", "200", "5", "1000", "17.9"});
        list.add(new String[]{"桃子", "300", "4", "1200", "21.4"});
        list.add(new String[]{"葡萄", "400", "3", "1200", "21.4"});
        list.add(new String[]{"西瓜", "500", "2", "1000", "17.9"});
        list.add(new String[]{"车厘子", "600", "1", "600", "10.7"});
        //根据数据生成表格
        inserInfo(table, list);
        //设置表格中所有单元格水平居中对齐
        poiUtils.setTableCenter(table);
    }

    //替换统计图数据
    public  void replaceChart(XWPFDocument document,String key){
        //模拟统计图数据
        //系列
        String[] series={"销售量(kg)","销售额(元)","净盈利额(元)"};
        //x轴
        String[] categories={"星期一","星期二","星期三","星期四","星期五","星期六","星期日"};
        List<Number[]> values = new ArrayList<>();
        //一周的销售量
        Number[] value1 = {60,80,74,52,66,88,90};
        //一周的销售额
        Number[] value2 = {450.2,652.1,554,384.6,486.5,688.9,711.1};
        //一周的净盈利额
        Number[] value3 = {200.2,326.4,266,159.5,222.2,355.5,369.5};

        values.add(value1);
        values.add(value2);
        values.add(value3);

        try {
            XWPFChart xChart = null;
            CTChart ctChart = null;
            for (POIXMLDocumentPart part : document.getCharts()) {
                if (part instanceof XWPFChart) {
                    xChart = (XWPFChart) part;
                    ctChart = xChart.getCTChart();
                    String chartTitle=getTitle(ctChart);
                    if (key.equalsIgnoreCase(chartTitle) ) {
                        generateChart(xChart, series,categories,values);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取模板中表格的标题
     * @param chart
     * @return
     */
    public String getTitle(CTChart chart) {
        CTTitle title = chart.getTitle();
        if (title != null) {
            CTTx tx = title.getTx();
            CTTextBody tb = tx.getRich();
            return tb.getPArray(0).getRArray(0).getT();
        }
        return "";
    }

    /**
     *
     * @param chart 模板中统计图对象
     * @param series 系列
     * @param categories x轴
     * @param values 具体的值
     */
    public abstract void generateChart(XWPFChart chart,String[]series,String[]categories,List<Number[]> values);

    // 替换图名称
    public abstract void replaceChartName(XWPFParagraph paragraph);

    /**
     * 把信息插入表格,因为每个章节的表格生成都一样,避免麻烦,所以统一用这个数据
     * @param table
     * @param list
     */
    private void inserInfo(XWPFTable table, List<String[]> list) {
        //设置表格宽度
        table.setWidth(10000);
        XWPFTableRow row = table.getRow(0);
        row.getCell(0).setText("指标");
        row.addNewTableCell().setText("销售数量");
        row.addNewTableCell().setText("单价");
        row.addNewTableCell().setText("销售总额");
        row.addNewTableCell().setText("销售额占比");

        for (int i = 0; i < list.size(); i++) {
            row = table.createRow();
            String[] obj = list.get(i);
            for (int j = 0; j < obj.length; j++) {
                row.getCell(j).setText(obj[j]);
            }
        }
    }
}
           

2.3 章节1

import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;

import java.util.List;

public class Section1 extends BaseSection{

    @Override
    public  void replaceSectionTitle(XWPFDocument document, XWPFParagraph paragraph) {
        String sectionTitle ="水果一";
        poiUtils.setLevelTitle1(document,paragraph,sectionTitle);
    }

    @Override
    public void replaceBody(XWPFParagraph paragraph) {
        String body ="我藏不住秘密,也藏不住忧伤,正如我藏不住爱你的喜悦,藏不住分离时的彷徨。" +
                "我就是这样坦然,你舍得伤,就伤。如果有一天,你要离开我,我不会留你,我知道你有" +
                "你的理由;如果有一天,你说还爱我,我会告诉你,其实我一直在等你;如果有一天,我们" +
                "擦肩而过,我会停住脚步,凝视你远去的背影,告诉自己那个人我曾经爱过。或许人一生可" +
                "以爱很多次,然而总有一个人,可以让我们笑得最灿烂,哭得最透彻,想得最深切。炊烟起" +
                "了,我在门口等你。夕阳下了,我在山边等你。叶子黄了,我在树下等你。月儿弯了,我在" +
                "十五等你。细雨来了,我在伞下等你。流水冻了,我在河畔等你。生命累了,我在天堂等你" +
                "。我们老了,我在来生等你。";
        poiUtils.setTextPro(paragraph,body);
    }


    @Override
    public void generateChart(XWPFChart chart, String[] series, String[] categories, List<Number[]> values) {
        String chartTitle="香蕉销售数据周统计图";
        final List<XDDFChartData> data = chart.getChartSeries();
        final XDDFBarChartData bar = (XDDFBarChartData) data.get(0);

        final int numOfPoints = categories.length;

        final String categoryDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));

        final XDDFDataSource<?> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
        for (int i = 0; i < values.size(); i++) {
            final String valuesDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, i + 1, i + 1));
            Number[] value = values.get(i);
            final XDDFNumericalDataSource<? extends Number> valuesData = XDDFDataSourcesFactory.fromArray(value, valuesDataRange, i + 1);
            XDDFChartData.Series ser;
            if (i < 3) {
                ser = bar.getSeries().get(i);
                ser.replaceData(categoriesData, valuesData);
            } else {
                ser = bar.addSeries(categoriesData, valuesData);
            }
            CellReference cellReference = chart.setSheetTitle(series[i], 1);
            ser.setTitle(series[i], cellReference);
        }

        chart.plot(bar);
        chart.setTitleText(chartTitle);
        chart.setTitleOverlay(false);
    }

    @Override
    public void replaceChartName(XWPFParagraph paragraph) {
        String chartName="香蕉销售统计图(柱状图)";
        poiUtils.setTableOrChartTitle(paragraph,chartName);
    }
}
           

2.4 章节2

import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;

import java.util.List;

public class Section2 extends BaseSection{

    @Override
    public void replaceSectionTitle(XWPFDocument document, XWPFParagraph paragraph) {
        String sectionTitle ="水果二";
        poiUtils.setLevelTitle1(document,paragraph,sectionTitle);
    }

    @Override
    public void replaceBody(XWPFParagraph paragraph) {
        String body ="无论是太阳拨开云雾,还是云雾遮挡太阳,都是彼此间的争斗。不要在晴空万里时," +
                "才说天空“蔚蓝如海”。我的前身本是大海中的一滴水,当我们的云层在天空中翻腾涌动时," +
                "这才是波涛汹涌的大海。即使卑微,也有自己的理想,也想留下瞬间的精彩。即便阳光把我由" +
                "一滴晶莹剔透的水转成一缕游丝般的水汽,也要在天空中展现出鲜艳曼妙的身影,留下瞬间的精彩" +
                ",也要和其他兄弟姐妹组成波涛万里的云海,凝成雨珠,滴落大海,在轮回中重生!";
        poiUtils.setTextPro(paragraph,body);
    }


    @Override
    public void insertTable(XWPFDocument document, XWPFParagraph paragraph) {
        super.insertTable(document, paragraph);
    }

    @Override
    public void generateChart(XWPFChart chart, String[] series, String[] categories, List<Number[]> values) {
        String chartTitle="香蕉销售数据周统计图";
        final List<XDDFChartData> data = chart.getChartSeries();
        XDDFPieChartData pie = (XDDFPieChartData) data.get(0);
        Number[]values1 = values.get(2);
        final int numOfPoints = categories.length;
        final String categoryDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));
        final String valuesDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 1, 1));

        final XDDFDataSource<?> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
        final XDDFNumericalDataSource<?> valuesData = XDDFDataSourcesFactory.fromArray(values1, valuesDataRange, 1);

        XDDFChartData.Series series1 = pie.getSeries().get(0);
        series1.replaceData(categoriesData, valuesData);
        series1.setTitle(series[0], chart.setSheetTitle(series[0], 0));

        chart.plot(pie);
        chart.setTitleText(chartTitle);
        chart.setTitleOverlay(false);
    }

    @Override
    public void replaceChartName(XWPFParagraph paragraph) {
        String chartName="香蕉销售统计图(饼图)";
        poiUtils.setTableOrChartTitle(paragraph,chartName);
    }
}
           

2.5 章节3

import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;

import java.util.List;

public class Section3 extends BaseSection{

    @Override
    public void replaceSectionTitle(XWPFDocument document, XWPFParagraph paragraph) {
        String sectionTitle ="水果三";
        poiUtils.setLevelTitle1(document,paragraph,sectionTitle);
    }

    @Override
    public void replaceBody(XWPFParagraph paragraph) {
    String body="在一次次水与云的交替变化中,蓦然发现无论自己是天边的一片云,还是大海中的一滴水," +
            "都是那毫不起眼的卑微之物,谁会去解读一片云的心绪,聆听一滴水的声音?经历了风起风落," +
            "也就习惯了云卷云舒,渐渐的我懂得了顺其自然,心境归于平和,不在向往风起云涌的气象," +
            "云淡风轻才是自己的本色。";
        poiUtils.setTextPro(paragraph,body);
    }

    @Override
    public void generateChart(XWPFChart chart, String[] series, String[] categories, List<Number[]> values) {
        String chartTitle="香蕉销售数据周统计图";
        final List<XDDFChartData> data = chart.getChartSeries();
        final XDDFLineChartData line = (XDDFLineChartData) data.get(0);

        final int numOfPoints = categories.length;

        final String categoryDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));

        final XDDFDataSource<?> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
        for (int i = 0; i < values.size(); i++) {
            final String valuesDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, i + 1, i + 1));
            Number[] value = values.get(i);
            final XDDFNumericalDataSource<? extends Number> valuesData = XDDFDataSourcesFactory.fromArray(value, valuesDataRange, i + 1);
            XDDFChartData.Series ser;
            if (i < 3) {
                ser = line.getSeries().get(i);
                ser.replaceData(categoriesData, valuesData);
            } else {
                ser = line.addSeries(categoriesData, valuesData);
            }
            CellReference cellReference = chart.setSheetTitle(series[i], 1);
            ser.setTitle(series[i], cellReference);
        }

        chart.plot(line);
        chart.setTitleText(chartTitle);
        chart.setTitleOverlay(false);
    }

    @Override
    public void replaceChartName(XWPFParagraph paragraph) {
        String chartName="香蕉销售统计图(折线图)";
        poiUtils.setTableOrChartTitle(paragraph,chartName);
    }
}
           

2.6 配置章节类和章节类路径

  • application.yml
#文档生成相关类配置
doc:
  section:
    section1: com.poi.docgenerate.Section1
    section2: com.poi.docgenerate.Section2
    section3: com.poi.docgenerate.Section3
           
  • PoiPropConfig.java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


import java.util.Map;

@ConfigurationProperties(prefix = "doc")
@Component
public class PoiPropsConfig {

    //把章节名作为key,章节类的全路径名作为value
    private Map<String,String> section;

    public void setSection(Map<String, String> section) {
        this.section = section;
    }

    public Map<String, String> getSection() {
        return section;
    }
    
    @Override
    public String toString() {
        for (Map.Entry<String,String> entry:section.entrySet()){
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+":"+value);
        }
       return null;
    }
}
           

2.7 工具类

import com.google.common.base.Strings;
import com.police.hunan.judge.entity.vo.DocGenerateVO;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.springframework.stereotype.Component;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class PoiUtils {

    private Map<String, Map<String, Object>> orderMap = new HashMap<String, Map<String, Object>>();

    /*把*/
    public static void copy(Object obj, Object dest) {
        // 获取属性
        BeanInfo sourceBean = null;
        try {
            sourceBean = Introspector.getBeanInfo(obj.getClass(), Object.class);
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        PropertyDescriptor[] sourceProperty = sourceBean.getPropertyDescriptors();

        BeanInfo destBean = null;
        try {
            destBean = Introspector.getBeanInfo(dest.getClass(), Object.class);
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        PropertyDescriptor[] destProperty = destBean.getPropertyDescriptors();
        try {
            for (int i = 0; i < sourceProperty.length; i++) {
                Object value = sourceProperty[i].getReadMethod().invoke(obj);
                if (value != null) {

                    for (int j = 0; j < destProperty.length; j++) {
                        if (sourceProperty[i].getName().equals(destProperty[j].getName()) && sourceProperty[i].getPropertyType() == destProperty[j].getPropertyType()) {
                            // 调用source的getter方法和dest的setter方法
                            destProperty[j].getWriteMethod().invoke(dest, value);
                            break;
                        }
                    }
                } else {
                    continue;
                }
            }
        } catch (Exception e) {
            try {
                throw new Exception("属性复制失败:" + e.getMessage());
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }
    
    /**
     * 设置一级标题内容及样式
     *
     * @param paragraph
     * @param text
     */
    public  void setLevelTitle1(XWPFDocument document, XWPFParagraph paragraph, String text) {
        /**1.将段落原有文本(原有所有的Run)全部删除*/
        deleteRun(paragraph);
        /**3.插入新的Run即将新的文本插入段落*/
        XWPFRun createRun = paragraph.insertNewRun(0);
        createRun.setText(text);
        XWPFRun separtor = paragraph.insertNewRun(1);
        /**两段之间添加换行*/
        separtor.setText("\r");
        createRun.setFontSize(16);
        createRun.setFontFamily("黑体");
        paragraph.setIndentationFirstLine(600);
        paragraph.setSpacingAfter(20);
        paragraph.setSpacingBefore(20);
        addCustomHeadingStyle(document, "标题1", 1);
        paragraph.setStyle("标题1");
    }

    /**
     * 设置二级标题内容及样式
     *
     * @param paragraph
     * @param text
     */
    public  void setLevelTitle2(XWPFDocument document, XWPFParagraph paragraph, String text) {
        deleteRun(paragraph);
        /**3.插入新的Run即将新的文本插入段落*/
        XWPFRun createRun = paragraph.insertNewRun(0);
        createRun.setText(text);
        XWPFRun separtor = paragraph.insertNewRun(1);
        /**两段之间添加换行*/
        separtor.setText("\r");
        createRun.setFontSize(16);
        createRun.setBold(true);
        createRun.setFontFamily("楷体_GB2312");
        paragraph.setIndentationFirstLine(600);
        paragraph.setSpacingAfter(10);
        paragraph.setSpacingBefore(10);
        addCustomHeadingStyle(document, "标题2", 2);
        paragraph.setStyle("标题2");
    }

    /**
     * 设置正文文本内容及样式
     *
     * @param paragraph
     * @param text
     */
    public  void setTextPro(XWPFParagraph paragraph, String text) {
        deleteRun(paragraph);
        /**3.插入新的Run即将新的文本插入段落*/
        XWPFRun createRun = paragraph.insertNewRun(0);
        createRun.setText(text);
        XWPFRun separtor = paragraph.insertNewRun(1);
        /**两段之间添加换行*/
        separtor.addBreak();
        createRun.addTab();
        createRun.setFontFamily("仿宋_GB2312");
        createRun.setFontSize(16);

        paragraph.setFirstLineIndent(20);
        paragraph.setAlignment(ParagraphAlignment.BOTH);
        paragraph.setIndentationFirstLine(600);
        paragraph.setSpacingAfter(10);
        paragraph.setSpacingBefore(10);
    }

    /**
     * 向段落添加文本
     *
     * @param paragraph
     * @param text
     */
    public  void addTextPro(XWPFParagraph paragraph, String text) {
        /**3.插入新的Run即将新的文本插入段落*/
        XWPFRun createRun = paragraph.createRun();
        createRun.setText(text);
        XWPFRun separtor = paragraph.createRun();
        /**两段之间添加换行*/
        separtor.addBreak();
        createRun.addTab();
        createRun.setFontFamily("仿宋_GB2312");
        createRun.setFontSize(16);
        paragraph.setFirstLineIndent(20);
        paragraph.setAlignment(ParagraphAlignment.BOTH);
        paragraph.setIndentationFirstLine(600);
        paragraph.setSpacingAfter(10);
        paragraph.setSpacingBefore(10);

        paragraph.addRun(createRun);
        paragraph.addRun(separtor);
    }

    /**
     * 设置表格标题内容及样式
     *
     * @param paragraph
     * @param text
     */
    public  void setTableOrChartTitle(XWPFParagraph paragraph, String text) {
        /**1.将段落原有文本(原有所有的Run)全部删除*/
       deleteRun(paragraph);
        XWPFRun createRun = paragraph.insertNewRun(0);
        createRun.setText(text);
        XWPFRun separtor = paragraph.insertNewRun(1);
        /**两段之间添加换行*/
        separtor.setText("\r");
        createRun.setFontFamily("楷体");
        createRun.setFontSize(16);
        createRun.setBold(true);
        paragraph.setSpacingAfter(10);
        paragraph.setSpacingBefore(10);
        paragraph.setAlignment(ParagraphAlignment.CENTER);
    }

    public void deleteRun(XWPFParagraph paragraph) {
        /*1.将段落原有文本(原有所有的Run)全部删除*/
        List<XWPFRun> runs = paragraph.getRuns();
        int runSize = runs.size();
        /*Paragrap中每删除一个run,其所有的run对象就会动态变化,即不能同时遍历和删除*/
        int haveRemoved = 0;
        for (int runIndex = 0; runIndex < runSize; runIndex++) {
            paragraph.removeRun(runIndex - haveRemoved);
            haveRemoved++;
        }
    }

    /**
     * 合并行
     *
     * @param table
     * @param col     需要合并的列
     * @param fromRow 开始行
     * @param toRow   结束行
     */
    public static void mergeCellVertically(XWPFTable table, int col, int fromRow, int toRow) {
        for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++) {
            CTVMerge vmerge = CTVMerge.Factory.newInstance();
            if (rowIndex == fromRow) {
                vmerge.setVal(STMerge.RESTART);
            } else {
                vmerge.setVal(STMerge.CONTINUE);
            }
            XWPFTableCell cell = table.getRow(rowIndex).getCell(col);
            CTTcPr tcPr = cell.getCTTc().getTcPr();
            if (tcPr != null) {
                tcPr.setVMerge(vmerge);
            } else {
                tcPr = CTTcPr.Factory.newInstance();
                tcPr.setVMerge(vmerge);
                cell.getCTTc().setTcPr(tcPr);
            }
        }
    }

    /**
     * 设置表格内容居中
     *
     * @param table
     */
    public void setTableCenter(XWPFTable table) {
        List<XWPFTableRow> rows = table.getRows();
        for (XWPFTableRow row : rows) {
            row.setHeight(400);
            List<XWPFTableCell> cells = row.getTableCells();
            for (XWPFTableCell cell : cells) {
                CTTc cttc = cell.getCTTc();
                CTTcPr ctPr = cttc.addNewTcPr();
                ctPr.addNewVAlign().setVal(STVerticalJc.CENTER);
                cttc.getPList().get(0).addNewPPr().addNewJc().setVal(STJc.CENTER);
            }
        }
    }

    public void init(XWPFDocument document) {
        //获取段落
        List<XWPFParagraph> paras = document.getParagraphs();

        for (int i = 0; i < paras.size(); i++) {
            XWPFParagraph para = paras.get(i);
//              System.out.println(para.getCTP());//得到xml格式
//              System.out.println(para.getStyleID());//段落级别
//              System.out.println(para.getParagraphText());//段落内容

            String titleLvl = getTitleLvl(document, para);//获取段落级别
            if ("a5".equals(titleLvl) || "HTML".equals(titleLvl) || "".equals(titleLvl) || null == titleLvl) {
                titleLvl = "8";
            }

            if (null != titleLvl && !"".equals(titleLvl) && !"8".equals(titleLvl)) {
                String t = titleLvl;
                String orderCode = getOrderCode(titleLvl);//获取编号
                String text = para.getParagraphText();
                text = orderCode + " " + text;
                List<XWPFRun> runs = para.getRuns();
                int runSize = runs.size();
                /**Paragrap中每删除一个run,其所有的run对象就会动态变化,即不能同时遍历和删除*/
                int haveRemoved = 0;
                for (int runIndex = 0; runIndex < runSize; runIndex++) {
                    para.removeRun(runIndex - haveRemoved);
                    haveRemoved++;
                }
                if ("1".equals(titleLvl)) {
                    setLevelTitle1(document, para, text);
                }
                if ("2".equals(titleLvl)) {
                    setLevelTitle2(document, para, text);
                }
            }
        }
    }

    /**
     * Word中的大纲级别,可以通过getPPr().getOutlineLvl()直接提取,但需要注意,Word中段落级别,通过如下三种方式定义:
     * 1、直接对段落进行定义;
     * 2、对段落的样式进行定义;
     * 3、对段落样式的基础样式进行定义。
     * 因此,在通过“getPPr().getOutlineLvl()”提取时,需要依次在如上三处读取。
     *
     * @param doc
     * @param para
     * @return
     */
    private String getTitleLvl(XWPFDocument doc, XWPFParagraph para) {
        String titleLvl = "";

        //判断该段落是否设置了大纲级别
        CTP ctp = para.getCTP();
        if (ctp != null) {
            CTPPr pPr = ctp.getPPr();
            if (pPr != null) {
                if (pPr.getOutlineLvl() != null) {
                    return String.valueOf(pPr.getOutlineLvl().getVal());
                }
            }
        }


        //判断该段落的样式是否设置了大纲级别
        if (para.getStyle() != null) {
            if (doc.getStyles().getStyle(para.getStyle()).getCTStyle().getPPr().getOutlineLvl() != null) {
                return String.valueOf(doc.getStyles().getStyle(para.getStyle()).getCTStyle().getPPr().getOutlineLvl().getVal());
            }


            //判断该段落的样式的基础样式是否设置了大纲级别
            if (doc.getStyles().getStyle(doc.getStyles().getStyle(para.getStyle()).getCTStyle().getBasedOn().getVal())
                    .getCTStyle().getPPr().getOutlineLvl() != null) {
                String styleName = doc.getStyles().getStyle(para.getStyle()).getCTStyle().getBasedOn().getVal();
                return String.valueOf(doc.getStyles().getStyle(styleName).getCTStyle().getPPr().getOutlineLvl().getVal());
            }
        }
        if (para.getStyleID() != null) {
            return para.getStyleID();
        }
        return titleLvl;
    }

    /**
     * 增加自定义标题样式。这里用的是stackoverflow的源码
     *
     * @param docxDocument 目标文档
     * @param strStyleId   样式名称
     * @param headingLevel 样式级别
     */
    private static void addCustomHeadingStyle(XWPFDocument docxDocument, String strStyleId, int headingLevel) {


        CTStyle ctStyle = CTStyle.Factory.newInstance();
        ctStyle.setStyleId(strStyleId);

        CTString styleName = CTString.Factory.newInstance();
        styleName.setVal(strStyleId);
        ctStyle.setName(styleName);

        CTDecimalNumber indentNumber = CTDecimalNumber.Factory.newInstance();
        indentNumber.setVal(BigInteger.valueOf(headingLevel));

        // lower number > style is more prominent in the formats bar
        ctStyle.setUiPriority(indentNumber);

        CTOnOff onoffnull = CTOnOff.Factory.newInstance();
        ctStyle.setUnhideWhenUsed(onoffnull);

        // style shows up in the formats bar
        ctStyle.setQFormat(onoffnull);

        // style defines a heading of the given level
        CTPPr ppr = CTPPr.Factory.newInstance();
        ppr.setOutlineLvl(indentNumber);
        ctStyle.setPPr(ppr);

        XWPFStyle style = new XWPFStyle(ctStyle);

        // is a null op if already defined
        XWPFStyles styles = docxDocument.createStyles();

        style.setType(STStyleType.PARAGRAPH);
        styles.addStyle(style);

    }

    /**
     * 获取标题编号
     *
     * @param titleLvl
     * @return
     */
    private String getOrderCode(String titleLvl) {
        String order = "";

        if ("0".equals(titleLvl) || Integer.parseInt(titleLvl) == 8) {//文档标题||正文
            return "";
        } else if (Integer.parseInt(titleLvl) > 0 && Integer.parseInt(titleLvl) < 8) {//段落标题

            //设置最高级别标题
            Map<String, Object> maxTitleMap = orderMap.get("maxTitleLvlMap");
            if (null == maxTitleMap) {//没有,表示第一次进来
                //最高级别标题赋值
                maxTitleMap = new HashMap<String, Object>();
                maxTitleMap.put("lvl", titleLvl);
                orderMap.put("maxTitleLvlMap", maxTitleMap);
            } else {
                String maxTitleLvl = maxTitleMap.get("lvl") + "";//最上层标题级别(0,1,2,3)
                if (Integer.parseInt(titleLvl) < Integer.parseInt(maxTitleLvl)) {//当前标题级别更高
                    maxTitleMap.put("lvl", titleLvl);//设置最高级别标题
                    orderMap.put("maxTitleLvlMap", maxTitleMap);
                }
            }

            //查父节点标题
            int parentTitleLvl = Integer.parseInt(titleLvl) - 1;//父节点标题级别
            Map<String, Object> cMap = orderMap.get(titleLvl);//当前节点信息
            Map<String, Object> pMap = orderMap.get(parentTitleLvl + "");//父节点信息

            if (0 == parentTitleLvl) {//父节点为文档标题,表明当前节点为1级标题
                int count = 0;
                //最上层标题,没有父节点信息
                if (null == cMap) {//没有当前节点信息
                    cMap = new HashMap<String, Object>();
                } else {
                    count = Integer.parseInt(String.valueOf(cMap.get("cCount")));//当前序个数
                }
                count++;
                order = count + "";
                cMap.put("cOrder", order);//当前序
                cMap.put("cCount", count);//当前序个数
                orderMap.put(titleLvl, cMap);

            } else {//父节点为非文档标题
                int count = 0;
                //如果没有相邻的父节点信息,当前标题级别自动升级
                if (null == pMap) {
                    return getOrderCode(String.valueOf(parentTitleLvl));
                } else {
                    String pOrder = String.valueOf(pMap.get("cOrder"));//父节点序
                    if (null == cMap) {//没有当前节点信息
                        cMap = new HashMap<String, Object>();
                    } else {
                        count = Integer.parseInt(String.valueOf(cMap.get("cCount")));//当前序个数
                    }
                    count++;
                    order = pOrder + "." + count;//当前序编号
                    cMap.put("cOrder", order);//当前序
                    cMap.put("cCount", count);//当前序个数
                    orderMap.put(titleLvl, cMap);
                }
            }

            //字节点标题计数清零
            int childTitleLvl = Integer.parseInt(titleLvl) + 1;//子节点标题级别
            Map<String, Object> cdMap = orderMap.get(childTitleLvl + "");//
            if (null != cdMap) {
                cdMap.put("cCount", 0);//子节点序个数
                orderMap.get(childTitleLvl + "").put("cCount", 0);
            }
        }
        return order;
    }
}

           

结果

POI生成word文档,包括标题,段落,表格,统计图(非图片格式)
POI生成word文档,包括标题,段落,表格,统计图(非图片格式)