天天看點

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文檔,包括标題,段落,表格,統計圖(非圖檔格式)