原文: freemarker導出復雜Excel
date: 2017-04-20 12:39:04
[TOC]
序言
用Freemarker做Excel導出確實很容易. 但是導出復雜Excel, 例如多行合并的還是費了一天時間
步驟
首先是pom依賴, 構建工具使用Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
準備模板
接下來, 制作ftl模板
在Office中編輯一個Excel文件, 這就是最后要生成的模板.

這里寫圖片描述
注意: 另存為xml格式. 不要直接改后綴名
拷貝到項目下, 修改后綴名為.ftl (這里我放到resources/templates目錄下)
用xml方式打開這個.ftl文件. 找到如下代碼
<Table ss:ExpandedColumnCount="4" ss:ExpandedRowCount="3" x:FullColumns="1"
x:FullRows="1" ss:DefaultColumnWidth="54" ss:DefaultRowHeight="13.5">
<Row>
<Cell ss:MergeAcross="3" ss:StyleID="s66"><Data ss:Type="String">人員列表</Data></Cell>
</Row>
<Row>
<Cell ss:StyleID="s67"/>
<Cell ss:StyleID="s67"><Data ss:Type="String">name</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">age</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">address</Data></Cell>
</Row>
<Row>
<Cell ss:StyleID="s67"><Data ss:Type="Number">1</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">zhangsan</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="Number">22</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">BeiJing</Data></Cell>
</Row>
</Table>
主要看上面這段, 對比你的Excel很容易看出: 每個就是一行, 每個是一個單元格
找到要循環(huán)的一行, 添加表達式和標簽
關于Freemarker的語法, 可以參考http://freemarker.foofun.cn/index.html
加完標簽后如下:
<Table ss:ExpandedColumnCount="4" ss:ExpandedRowCount="${userListSize}" x:FullColumns="1"
x:FullRows="1" ss:DefaultColumnWidth="54" ss:DefaultRowHeight="13.5">
<Row>
<Cell ss:MergeAcross="3" ss:StyleID="s66"><Data ss:Type="String">人員列表</Data></Cell>
</Row>
<Row>
<Cell ss:StyleID="s67"/>
<Cell ss:StyleID="s67"><Data ss:Type="String">姓名</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">年齡</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">地址</Data></Cell>
</Row>
<#assign index = 0 >
<#list userList as u>
<#assign index = index + 1 >
<Row>
<Cell ss:StyleID="s67"><Data ss:Type="Number">${index}</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">${u.name}</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="Number">${u.age}</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">${u.address}</Data></Cell>
</Row>
</#list>
</Table>
注意ExpandedRowCount的值可以變量傳進來, 如果不好計算可以寫一個很大的值.
自己寫的值有什么影響我沒測試…
準備數據源
對于你要動態(tài)生成的List, 里面可以放Map, 也可以放對象, 都行.
這里我先用Map, 等下用對象
- 模擬獲取數據
/**
* 構造user數據List<Map<String, Object>>
*/
private static List<Map<String, Object>> getUserList(){
List<Map<String, Object>> returnList = new ArrayList<Map<String, Object>>();
Map<String,Object> map1 = new HashMap<String, Object>();
Map<String,Object> map2 = new HashMap<String, Object>();
map1.put("name", "張三");
map1.put("age", "18");
map1.put("address", "廣東");
map2.put("name", "王五");
map2.put("age", "22");
map2.put("address", "北京");
returnList.add(map1);
returnList.add(map2);
return returnList;
}
- 控制器
@SuppressWarnings("unchecked")
@RequestMapping(value="/exportExcel", method=RequestMethod.GET)
public void exportExcelByFreeMarker(HttpServletRequest request, HttpServletResponse response) {
try {
List<Map<String, Object>> userList= this.getUserList();
configuration.setDefaultEncoding("UTF-8");
configuration.setTemplateUpdateDelayMilliseconds(0);
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
//獲取模板
Template template = configuration.getTemplate("userlist.ftl");
Map<String,Object> root = new HashMap<String,Object>();
root.put("userList", userList);
root.put("userListSize", String.valueOf(userList.size()+2));
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
Date today = new Date();
String fileName = "人員列表" + sdf.format(today);//以今天的日期為文件名
response.setContentType("application/msexcel;charset=UTF-8");
response.setHeader("Content-disposition","attachment;filename=\""+new String((fileName+".xls").getBytes("GBK"),"ISO8859-1")+"\"");
//response字符流轉換成字節(jié)流,template需要字節(jié)流作為輸出
OutputStream outputStream = response.getOutputStream();
OutputStreamWriter outputWriter = new OutputStreamWriter(outputStream,"UTF-8");
Writer writer = new BufferedWriter(outputWriter);
template.process(root, writer);
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
在瀏覽器中輸入請求地址就可以導出了.
復雜Excel
生成Excel也有很多復雜的情況, 比如帶圖片, 跨行合并等.
這里例多行合并的, 例如:

這里寫圖片描述
生成的xml:
<Table ss:ExpandedColumnCount="5" ss:ExpandedRowCount="99999" x:FullColumns="1"
x:FullRows="1" ss:DefaultColumnWidth="54" ss:DefaultRowHeight="13.5">
<Row>
<Cell ss:MergeAcross="3" ss:StyleID="s66"><Data ss:Type="String">人員列表</Data></Cell>
<Cell ss:StyleID="s62"/>
</Row>
<Row>
<Cell ss:StyleID="s67"/>
<Cell ss:StyleID="s67"><Data ss:Type="String">姓名</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">年齡</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">孩子</Data></Cell>
<Cell ss:StyleID="s69"><Data ss:Type="String">地址</Data></Cell>
</Row>
<Row>
<Cell ss:MergeDown="1" ss:StyleID="m87387632"><Data ss:Type="Number">1</Data></Cell>
<Cell ss:MergeDown="1" ss:StyleID="m87387612"><Data ss:Type="String">張三</Data></Cell>
<Cell ss:MergeDown="1" ss:StyleID="m87387592"><Data ss:Type="Number">22</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">小張</Data></Cell>
<Cell ss:MergeDown="1" ss:StyleID="m87387572"><Data ss:Type="String">北京</Data></Cell>
</Row>
<Row>
<Cell ss:Index="4" ss:StyleID="s67"><Data ss:Type="String">二張</Data></Cell>
</Row>
</Table>
仔細觀察這段代碼, 跟上面的不帶合并的xml進行對比:
- 每個帶合并的Cell都帶有一個屬性MergeDown, 它的值為 (合并格數-1)
- 合并的格數是通過一個對象的循環(huán)來得到, 比如這里的”孩子”字段
- 擴展的Row會有一個Index屬性, 它的值就是字段的列數, 這里是4
分析好之后開始加標簽:
<Table ss:ExpandedColumnCount="5" ss:ExpandedRowCount="4" x:FullColumns="1"
x:FullRows="1" ss:DefaultColumnWidth="54" ss:DefaultRowHeight="13.5">
<Row>
<Cell ss:MergeAcross="3" ss:StyleID="s66"><Data ss:Type="String">人員列表</Data></Cell>
<Cell ss:StyleID="s62"/>
</Row>
<Row>
<Cell ss:StyleID="s67"/>
<Cell ss:StyleID="s67"><Data ss:Type="String">姓名</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">年齡</Data></Cell>
<Cell ss:StyleID="s67"><Data ss:Type="String">孩子</Data></Cell>
<Cell ss:StyleID="s69"><Data ss:Type="String">地址</Data></Cell>
</Row>
<#assign index = 0 >
<#list userBoList as u>
<#assign index = index + 1 >
<#assign num = u.children?size-1>
<#if num lt 0>
<#assign num = 0>
</#if>
<Row>
<Cell ss:MergeDown="${num}" ss:StyleID="m87387632"><Data ss:Type="Number">${index}</Data></Cell>
<Cell ss:MergeDown="${num}" ss:StyleID="m87387612"><Data ss:Type="String">${u.name}</Data></Cell>
<Cell ss:MergeDown="${num}" ss:StyleID="m87387592"><Data ss:Type="Number">${u.age}</Data></Cell>
<!-- 在children的List中取出第一個 -->
<#list u.children as firstChildren>
<#if firstChildren_index == 0>
<Cell ss:StyleID="s67"><Data ss:Type="String">${firstChildren.name}</Data></Cell>
</#if>
</#list>
<Cell ss:MergeDown="${num}" ss:StyleID="m87387572"><Data ss:Type="String">${u.address}</Data></Cell>
</Row>
<!-- 從第一個之后開始取 -->
<#list u.children as c>
<#if (u.children?size > 1 && c_index > 0 )>
<Row>
<Cell ss:Index="4" ss:StyleID="s67"><Data ss:Type="String">${c.name}</Data></Cell>
</Row>
</#if>
</#list>
</#list>
</Table>
注意:
MergeDown這個屬性中判斷了是否小于0, 親測在office中<0會導致Excel報錯, WPS不會, 最好還是判斷一下
ExpandedRowCount可以給個很大的值, 最好還是通過程序來傳大小
在這個Excel中需要兩個List:
User: List<UserBo>
Children: List<ChildrenBo>
User -> Children : 1->N
也可以是Map: List<Map<String, Object>>