java生成word文檔
最近得到一個需求:按用戶提供的模板生成分析報告,並讓用戶可以在網頁上導出。這個功能以前沒做過,但是好像聽說過freemarker。於是乎,開始了我的百度之旅。
一、word文檔的本質
我也是最近才知道,word文檔的本質原來是一個壓縮文件。不信你看,將.docx文件修改文件后綴為.zip

然后解壓縮得到了這些文件,這些就是組成word文檔的所有文件。其中word文件夾下是主要內容


其中,document.xml中是關於文檔內容的設置,相當於網頁里面的html文件一樣。_rels文件夾下的document.xml.rels文件是圖片配置信息。media文件夾下是文檔中所有圖片的文件,其他的應該是類似於網頁里面的CSS文件,設置樣式的。所以document.xml就是我們要修改的了。這樣的操作就相當於網頁已經編寫好了,只差從后台傳送數據到前端展示了。
二、創建freemarker模板
freemarker是一個模板引擎,百度是這樣介紹的:
FreeMarker是一款模板引擎: 即一種基於模板和要改變的數據, 並用來生成輸出文本(HTML網頁、電子郵件、配置文件、源代碼等)的通用工具。 它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發產品的組件。
說白了就是跟前端頁面的變量綁定是一樣的,用過vuejs的都知道,前端使用“{{name}}”雙括號括起來的變量可以通過傳值改變頁面上數據。freemarker也是這樣,通過在document.xml中使用“${name}”dollar符大括號括起來的變量也可以通過傳值改變模板文件內容。清楚了這點就好辦了。
將document.xml文件放到IDEA項目中的templates文件夾下,然后按Ctrl+Alt+L鍵格式化xml內容,將需要動態修改的地方用“${}”括起來,如下

有的時候變量會被拆分成兩個,就要麻煩點把兩個中間的多余部分全都刪掉,然后在用符號括起來。這點相信大家都能理解。
以上模板創建就結束了,是不是很簡單。
三、代碼實現
模板搞定了,怎么根據模板生成文檔呢?關鍵步驟來了。
1.首先我們要將模板中的變量賦值,生成新的文件。
2.將生成的文件寫入壓縮文件。上面已經說了,word文檔本質就是壓縮文件。
3.將.docx文檔的其他內容也寫入壓縮文件。
4.將壓縮文件寫入word文檔,這就是最后生成的文檔。
導入依賴:
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
主要用到下面的工具類:
public class FreeMarkUtils {
private static Logger logger = LoggerFactory.getLogger(FreeMarkUtils.class);
public static Configuration getConfiguration(){
//創建配置實例
Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
//設置編碼
configuration.setDefaultEncoding("utf-8");
configuration.setClassForTemplateLoading(FreeMarkUtils.class, "/templates");//此處設置模板存放文件夾名稱
return configuration;
}
/**
* 獲取模板字符串輸入流
* @param dataMap 參數,鍵為綁定變量名,值為變量值
* @param templateName 模板名稱
* @return
*/
public static ByteArrayInputStream getFreemarkerContentInputStream(Map dataMap, String templateName) {
ByteArrayInputStream in = null;
try {
//獲取模板
Template template = getConfiguration().getTemplate(templateName);
StringWriter swriter = new StringWriter();
//生成文件
template.process(dataMap, swriter);
in = new ByteArrayInputStream(swriter.toString().getBytes("utf-8"));//這里一定要設置utf-8編碼 否則導出的word中中文會是亂碼
} catch (Exception e) {
e.printStackTrace();
logger.error("模板生成錯誤!");
}
return in;
}
//outputStream 輸出流可以自己定義 瀏覽器或者文件輸出流
public static void createDocx(Map dataMap, OutputStream outputStream) {
ZipOutputStream zipout = null;
try {
//內容模板,傳值生成新的文件輸入流documentInput
ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, "document.xml");
//最初設計的模板,原word文件生成File對象
File docxFile = new File(WordUtils.class.getClassLoader().getResource("templates/demo.docx").getPath());//模板文件名稱
if (!docxFile.exists()) {
docxFile.createNewFile();
}
ZipFile zipFile = new ZipFile(docxFile);//獲取原word文件的zip文件對象,相當於解壓縮了word文件
Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();//獲取壓縮文件內部所有內容
zipout = new ZipOutputStream(outputStream);
//開始覆蓋文檔------------------
int len = -1;
byte[] buffer = new byte[1024];
while (zipEntrys.hasMoreElements()) {//遍歷zip文件內容
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
zipout.putNextEntry(new ZipEntry(next.getName()));//這步相當於創建了個文件,下面是將流寫入這個文件
if ("word/document.xml".equals(next.getName())) {//如果是word/document.xml由我們輸入
if (documentInput != null) {
while ((len = documentInput.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
documentInput.close();
}
} else {
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}else{//這里設置圖片信息,針對要顯示的圖片
zipout.putNextEntry(new ZipEntry(next.getName()));
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
logger.error("word導出失敗:"+e.getStackTrace());
}finally {
if(zipout!=null){
try {
zipout.close();
} catch (IOException e) {
logger.error("io異常");
}
}
if(outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
logger.error("io異常");
}
}
}
}
}
四、插入圖片
通過傳值已經可以基本完成生成文檔的功能了,但是用戶還要求要在文檔中生成統計分析圖。知道文檔本質的我馬上想出了辦法,但是這個就稍微有點麻煩了。剛剛說了,media文件夾下存放的是文檔中所有的圖片。我可以在word文檔要生成統計圖的地方先用圖片占好位置,調好大小。然后解壓后看這個圖片在media文件夾中叫什么名字。最后在代碼生成的時候,跳過原文件圖片的寫入替換成我生成的圖片就可以了。核心代碼如下:
while (zipEntrys.hasMoreElements()) {
ZipEntry next = zipEntrys.nextElement();
InputStream is = zipFile.getInputStream(next);
if (next.toString().indexOf("media") < 0) {
...省略
}else{//這里設置圖片信息,針對要顯示的圖片
zipout.putNextEntry(new ZipEntry(next.getName()));
if(next.getName().indexOf("image1.png")>0){//例如要用一張圖去替換模板中的image1.png
if(dataMap.get("image1")!=null){
byte[] bys = Base64Util.decode(dataMap.get("image1").toString());
zipout.write(bys,0,bys.length);
}else{
while ((len = is.read(buffer)) != -1) {
zipout.write(buffer, 0, len);
}
is.close();
}
}
}
}
注意:這個Base64Util是一個將圖片文件轉化為base64編碼字符串和將base64編碼字符串轉換為字節數組的工具類。這里涉及到圖片文件的本質,圖片的本質是一個二進制文件,二進制文件可以轉換成字節數組,而字節數組又可以和字符串互相轉換。這個知道就好,這里我把生成的圖片的字符串存入了集合中,寫入文檔的時候將字節數組寫入。這樣原圖片就被替換成了生成的圖片了。這里直接將新的圖片文件寫入也是一樣的效果。
