如何用Java語言優雅地匯出Word文件

彈指時間發表於2020-10-18

 

前言

在日常的開發工作中,我們時常會遇到匯出Word文件報表的需求,比如公司的財務報表、醫院的患者統計報表、電商平臺的銷售報表等等。

匯出Word方式多種多樣,通常有以下幾種方式:

    1. 使用第三方Java工具類庫Hutool的Word工具類,參考網址為https://www.hutool.cn/docs/#/poi/Word生成-Word07Writer;

    2. 利用Apache POI和FreeMarker模板引擎;

    3. 第三方報表工具。

上面的幾種方式雖然可以實現Word文件的匯出,但有以下缺點

    第一種方式操作簡單,但也只能生成簡單的Word文件,無法生成有表格的Word文件;

    第二種方式可以生成複雜的Word文件,但是還要進行Word轉xml,xml轉ftl的雙重轉換,不適合內容經常變更的Word文件;

    第三種方式有時候不適合對格式要求嚴格的文件。

那麼,有沒有既簡單又高效的匯出Word的方法呢?答案是肯定有的。接下來我就來介紹一種用Java語言實現的,通過XDocReportFreeMarker模板引擎生成Word文件的方法。

 

準備環境

開發語言:

Java7及以上的版本。

開發工具:

Eclipse/Idea。

第三方依賴庫:

XDocReport、POI、Freemarker。

模板語言:

FreeMarker。

Word編輯器:

Microsoft 365或其他版本較高的Word編輯器。

 

示例Word模板

                     

製作模板

Word模板如上圖,可以看到,結構比較簡單,包括兩個部分,第一部分是純文字和數字,第二部分主要是表格。我們在實際的開發過程中生成的報表幾乎都是動態生成的,所以模板中的數字和表格裡的資料都要替換成我們後臺的實際資料。

替換Word模板中的動態變數,我們需要掌握兩個知識點:

    1.Word文件中的Word域,word域是引導Word在文件中自動插入文字、圖形、頁碼或其他資訊的一組程式碼。在這裡我們可以把         Word域理解成識別符號,這個識別符號表示Word文件中要被替換的內容;

    2.FreeMarker模板下的變數表示式,比如用${city}替換Word示例模板中的北京市。

瞭解了以上兩個概念後,我們就可以動手編輯Word模板了,步驟如下:

1. 首先在Word模板中選中要替換的文字,在這兒拿標題中的"北京市"為例,然後鍵盤使用 Ctrl + F9 組合鍵將其設定為域,此時文字會被"{}"包圍,接著滑鼠右鍵選擇【編輯域(E)...】:

2. 在彈出的對話方塊中,類別選擇“郵件合併”,域名選擇 "MergeField",域屬性中的域名填入模版表示式${city},點選【確定】按鈕:

3. 編輯後的效果如下:

4. 掌握替換文字的方法後,我們可以把Word模板第一部分需要替換的內容都替換成模板變數:

Word模板中表格資料的處理

表格中的資料實質上就是對集合的遍歷。

表格資料的處理其實和上面對文字內容的處理是類似的,只不過要在Word模板中加上集合的變數,Java程式碼中也要有對集合進行特對的處理(這個在後面的程式碼展示部分會說)。

具體操作步驟如下:

1. 選定表格中要替換的文字,然後鍵盤使用 Ctrl + F9 組合鍵將其設定為域,接著滑鼠右鍵選擇【編輯域(E)...】:

2. 在彈出的對話方塊中,類別選擇“郵件合併”,域名選擇 "MergeField",域屬性中的域名填入模版表示式${goods.num},點選【確定】按鈕;

3. 重複步驟2,替換表格中的其他文字內容:

後臺程式碼

新增依賴到pom.xml檔案

<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.jxls</groupId>
    <artifactId>jxls</artifactId>
    <version>2.6.0</version>
    <exclusions>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.jxls</groupId>
    <artifactId>jxls-poi</artifactId>
    <version>1.2.0</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.core</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.document</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.template</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.23</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>

編寫Java程式碼

package com.tzsj.test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
import io.renren.entity.Goods;

@Controller
@RequestMapping("/word")
public class WordTest {
	@Test
	public void test() throws IOException, XDocReportException {
		generateWord();
	}
	
	public void generateWord() throws IOException, XDocReportException {
		//獲取Word模板,模板存放路徑在專案的resources目錄下
	    InputStream ins = this.getClass().getResourceAsStream("/模板.docx");
        //註冊xdocreport例項並載入FreeMarker模板引擎
        IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins,     
                               TemplateEngineKind.Freemarker);
        //建立xdocreport上下文物件
        IContext context = report.createContext();
        
        //建立要替換的文字變數
        context.put("city", "北京市");
        context.put("startDate", "2020-09-17");
        context.put("endDate", "2020-10-16");
        context.put("totCnt", 3638763);
        context.put("totAmt", "6521");
        context.put("onCnt", 2874036);
        context.put("onAmt", "4768");
        context.put("offCnt", 764727);
        context.put("offAmt", "1753");
        context.put("typeCnt", 36);
        
        List<Goods> goodsList = new ArrayList<Goods>();
        Goods goods1 = new Goods();
        goods1.setNum(1);
        goods1.setType("臭美毀膚");
        goods1.setSv(675512);
        goods1.setSa("589");
        Goods goods2 = new Goods();
        goods2.setNum(2);
        goods2.setType("女裝");
        goods2.setSv(602145);
        goods2.setSa("651");
        Goods goods3 = new Goods();
        goods3.setNum(3);
        goods3.setType("手機");
        goods3.setSv(587737);
        goods3.setSa("866");
        Goods goods4 = new Goods();
        goods4.setNum(4);
        goods4.setType("傢俱裝潢");
        goods4.setSv(551193);
        goods4.setSa("783");
        Goods goods5 = new Goods();
        goods5.setNum(5);
        goods5.setType("食物飲品");
        goods5.setSv(528604);
        goods5.setSa("405");
        goodsList.add(goods1);
        goodsList.add(goods2);
        goodsList.add(goods3);
        goodsList.add(goods4);
        goodsList.add(goods5);
        context.put("goods", goodsList);
        
        //建立欄位後設資料
        FieldsMetadata fm = report.createFieldsMetadata();
        //Word模板中的表格資料對應的集合型別
        fm.load("goods", Goods.class, true);
       
        //輸出到本地目錄
        FileOutputStream out = new FileOutputStream(new File("D://商品銷售報表.docx"));
        report.process(context, out);
	}

}

Word模板中生成序號

給表格資料新增序號是通過後臺程式碼生成的,比如上面的"goods1.setNum(1)"這段程式碼,其實也可以在Word模板中設定對應的域變數來實現序號的填充。

語法如下:

@before-row[#list sequence as item]
    item?index
@after-row[/#list]

在表格中新增上面的表示式,XDocReport就會自動解析並生成序號,表格中的其他欄位也需要進行相應的改動:

 

提示:

1. 序號的表示式要拆成三個域,如下圖,要把這三部分分別設定成域;

     設定完成的結果參考上面表格中的序號表示式,表示式中"item?index+1"是因為序號是從0開始的,所以要加1

2. 表格中除序號的列需要改成item.xxx而不是之前的goods.xxx:

3. 生成效果如下:

建議:序號最好在後臺生成,用序號表示式生成的序號列會佔用比較大的空間,對資源有所浪費。

補充

1. JavaWeb專案中通常是通過瀏覽器下載的方式來下載Word文件,此時只需要把之前下載到本地的程式碼改成瀏覽器端下載的程式碼即可:

//輸出到本地目錄
//FileOutputStream out = new FileOutputStream(new File("D://商品銷售報表.docx"));
//report.process(context, out);

//瀏覽器端下載
response.setCharacterEncoding("utf-8");  
response.setContentType("application/msword");  
String fileName = "商品銷售報表.docx";  
response.setHeader("Content-Disposition", "attachment;filename="  
				    .concat(String.valueOf(URLEncoder.encode(fileName, "UTF-8"))));  
report.process(context, response.getOutputStream());

2. Word模板中的表格的長度最好充滿Word文件的左右兩邊,否則如果表格下面還有其他文字內容,下面的文字內容會自動填充到表格的縫隙處,而且會對下面的文字內容進行覆蓋。

 

加餐

其實,匯出Word模板,上面的模板和程式碼已經夠用了,但也有少數模板需要新增圖片和圖形(比如餅狀圖)。

 

製作圖片

圖片的生成不使用編輯域,使用模板圖片和Word的書籤功能,而且需要在後設資料中加入圖片型別的程式碼,以下為具體步驟:

1. 在Word模版中需要插入圖片的位置插入一張模版圖片,然後滑鼠單擊模板圖片插入一個書籤,設定書籤名稱,比如img1, 最後點選【新增】按鈕:

 2. 如果需要插入多個圖片,就在需要插入圖片的位置插入多個模板圖片並插入書籤設定對應的書籤名稱即可,後臺程式碼如下: 

package com.tzsj.test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;

public class ImgTest {

	@Test
	public void test() throws IOException, XDocReportException {
		generateWordForImg();
	}
	
	
	public void generateWordForImg() throws IOException, XDocReportException {
		//獲取Word模板,模板存放路徑在專案的resources目錄下
	    InputStream ins = this.getClass().getResourceAsStream("/圖片.docx");
        //註冊xdocreport例項並載入FreeMarker模板引擎
        IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, 
                               TemplateEngineKind.Freemarker);
        //建立xdocreport上下文物件
        IContext context = report.createContext();
        
        FieldsMetadata fm = report.createFieldsMetadata();
        //後設資料中加入圖片
        fm.addFieldAsImage("img1");
        fm.addFieldAsImage("img2");
        
        //獲取圖片
        InputStream img1 = this.getClass().getResourceAsStream("/11.jpg");
        InputStream img2 = this.getClass().getResourceAsStream("/33.jpg");
        
        //把圖片新增到上下文物件
        context.put("img1", img1);
        context.put("img2", img2);
        
        //輸出到本地目錄
        FileOutputStream out = new FileOutputStream(new File("D://圖片報表.docx"));
        report.process(context, out);
	}

}

 3. 匯出效果如下:

 

製作圖形

要在Word文件中生成柱狀圖、餅狀圖等圖形,需要在專案中引入第三方繪圖工具,在這裡使用xchart來演示在Word中生成餅狀圖圖形。

生成餅狀圖和生成圖片的方法很類似,具體步驟如下:

1. 在Word模版中需要插入圖片的位置插入一張模版圖片,然後滑鼠單擊模板圖片插入一個書籤,設定書籤名稱,比如chart,最後點選【新增】按鈕:

2. 編寫程式碼:

2.1 在pom.xml檔案中新增xchart的依賴:

<dependency>
	<groupId>org.knowm.xchart</groupId>
	<artifactId>xchart</artifactId>
	<version>3.5.4</version>
</dependency>

2.2 後臺程式碼:

package com.tzsj.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.PieChart;
import org.knowm.xchart.PieChartBuilder;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;

public class ChartTest {

	@Test
	public void test() throws IOException, XDocReportException {
		generateWordForChart();
	}
	
	
	public void generateWordForChart() throws IOException, XDocReportException {
		//獲取Word模板,模板存放路徑在專案的resources目錄下
	    InputStream ins = this.getClass().getResourceAsStream("/餅圖.docx");
        //註冊xdocreport例項並載入FreeMarker模板引擎
        IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins,                 
                                 TemplateEngineKind.Freemarker);
        
        //建立xdocreport上下文物件
        IContext context = report.createContext();
        
        FieldsMetadata fm = report.createFieldsMetadata();
        //後設資料中加入圖片
        fm.addFieldAsImage("chart");
        
        PieChart chart = new PieChartBuilder().width(800).height(620)
                                .title("銷售餅圖").build();
        
        //給餅圖設定對應的值
        chart.addSeries("臭美毀膚", 589);
        chart.addSeries("女裝", 651);
        chart.addSeries("手機", 866);
        chart.addSeries("家居裝潢", 783);
        chart.addSeries("食物飲品", 405);
        
        //生成餅圖
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BitmapEncoder.saveBitmap(chart, baos, BitmapEncoder.BitmapFormat.JPG);
        
        //把餅圖新增到上下文物件
        context.put("chart", new ByteArrayImageProvider(baos.toByteArray()));
        
        //輸出到本地目錄
        FileOutputStream out = new FileOutputStream(new File("D://餅圖報表.docx"));
        report.process(context, out);
	}


}

3. 匯出效果如下:

總結

這就是用Java語言實現,結合XDocReportFreeMarker模板引擎生成Word文件的方法。希望能給致力於開發的小夥伴帶來一絲絲幫助。

如果您在操作過程中遇到什麼問題,可以隨時聯絡我並給我留言。本人的微信公眾號名稱是彈指時間 。微信二維碼如下圖:

 

相關文章