如何通过 Freemark 优雅地生成那些花里胡哨的复杂样式 Excel 文件?
欢迎关注个人微信公众号: 小哈学Java, 文末分享阿里 P8 高级架构师吐血总结的 《Java 核心知识整理&面试.pdf》资源链接!!
个人网站: https://www.exception.site/essay/how-to-create-complex-style-excel-with-freemark
一、背景
小哈最近这段时间开始负责一个新的产品:下载中心。啥玩意这是?
产品的目的其实就是统一管控各业务组文件下载功能(包括一些海量数据的导出,文件合并上传等),项目组不用自己再去实现各式各样的文件(PDF, Word, Excel)生成, 统一对接下载中心,由下载中心统一完成文件的生成、合并、上传、下载流程。
问题来了,这里面包括一些复杂文件的生成,如带有复杂样式的 Excel 文件,比如下面这个样子的:
这种复杂样式的 Excel, 如果说放到各个业务线去实现还是好办的,因为站在各个业务组的角度,场景变化不会太多,按照文件格式,代码写死即可。
但是站在下载中心的角度,因为需要对接各个业务中心,每个业务中心生成的样式都不一样,不可能每个业务组接进来,我都得定制的写一套生成代码吧!这显然也不合常理!
那么,有没有什么一劳永逸的办法呢?答案是肯定的!
二、实现思路
要说实现方式,你的脑海里可能第一会想到传统的 Apache poi,jxl ,亦或者是阿里出品 EasyExcel 等等。
PS: 关于阿里的 EasyExcel, 小哈之前有分享过 ,没看过的小伙伴们,可以看下《7 行代码优雅地实现 Excel 文件生成&下载功能》。
对于这种复杂样式,要是用 Apache poi, jxl, 阿里 EasyExcel 去实现,不可避免的,代码肯定会非常繁琐。
有没有啥优雅(偷懒的)的方式呢?
其实我们可以通过视图引擎 Freemark、Velocity 来帮我们生成复杂样式 Excel 文件,无需关心花里胡哨的复杂样式,只关注于填充数据即可。接下来,我们以 Freemark 作为示例来讲解,如何生成这个复杂样式的 Excel 文件。
拓展阅读: 什么是 Freemark ?
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
其实,对于Java 后端来说,它更常被用来服务端动态渲染 html 页面返回给浏览器。前些年还比较火热,近些年因为前后端分离的火热,也开始慢慢淡出视野了。
三、快速上手
3.1 添加依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
注意: 小哈这里基于 Spring Boot 写的测试代码,版本号可以无需指定,否则,你需要手动指定好版本号。
3.2 导出 xml 模板文件
首先,将复杂样式的 Excel 文件另存为 .xml 视图模板,如下图所示:
打开 xml 模板文件,可以清晰的看到里面定义了各种节点,节点描述了整个 Excel 的样式结构, 如下图所示:
3.3 填充占位符
再回过头来看下之前那个复杂 Excel 文件, 观察一下哪些单元格的值需要动态设置:
图中用红色特意标注出来了。
在刚刚另存为的 xml 模板文件中填写 freemark 表达式,考虑到这里只是个示例 Demo, 仅仅选取几个示例单元格来填写占位符,如下所示:
订单标题:
其他需要动态填充的单元格:
PS: xml 文件中,
<Row>节点代表一行,<Cell>代表一个单元格。
在需要动态填充数据的地方,加上相关 freemark 表达式,如 ${commodity.name!},如下所示:
<Row ss:AutoFitHeight="0" ss:Height="54">
    <Cell ss:StyleID="s18"><Data ss:Type="String">1</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.name!}</Data></Cell>
    <Cell ss:StyleID="s18"/>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.num!}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.num1!}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">22</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">44</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">55</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">盒</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price!}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price2!}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.price3!}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.timestamp}</Data></Cell>
    <Cell ss:StyleID="s18"><Data ss:Type="String">${commodity.createTime?string('yyyy-MM-dd HH:mm:ss')}</Data></Cell>
    <Cell ss:StyleID="s18"/>
</Row>
按照服务端数据模型的定义,填写好相应的字段名称,再对照下后台 Commodity 商品类的定义:
这个商品类中,我们定义了不同类型的字段,如 String、int、Integer、Double、Float、金额类型 BigDecimal、日期类型 Date 等,用以测试对不同数据类型的兼容性。
确认相关属性字段名无误后,再来看下 freemark 生成 Excel 的核心代码:
package site.exception.springbootfreemarkexcel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import site.exception.springbootfreemarkexcel.entity.Commodity;
import java.io.File;
import java.io.FileWriter;
import java.math.BigDecimal;
import java.util.*;
/**
 * @author 犬小哈 (微信公众号: 小哈学Java)
 * @site www.exception.site
 * @date 2019/5/21
 * @time 上午10:57
 * @discription
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootFreemarkExcelApplicationTests {
	@Test
	public void createExcelByFreemark() throws Exception {
		Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
		// 设置模板文件的父目录
		configuration.setDirectoryForTemplateLoading(new File("/Users/a123123/Work/tmp_files"));
		// 加载模板文件
		Template template = configuration.getTemplate("/excel_template.xml", "UTF-8");
		// 数据准备,可以是从数据库中查询,这里为了方便演示,手动 new 了
		Map<String, Object> data = new HashMap<>();
		data.put("title", "测试标题1");
		List<Commodity> commodities = new ArrayList<>();
		Commodity commodity = Commodity.builder()
				.name("name1").num(11).num1(111)
				.price(new BigDecimal(11.1)).price2(new Double(11.11))
				.price3(new Float(11.1111))
				.createTime(new Date())
				.timestamp(System.currentTimeMillis())
				.build();
		commodities.add(commodity);
		// 生成 excel 文件
		template.process(data, new FileWriter("/Users/a123123/Work/tmp_files/excelByFreemark.xls"));
	}
}
可以看到生成复杂样式的 Excel 的代码非常简洁。关于每一行代码什么意思,注释已经说得很清楚了,这里就不加以说明了。
运行单元测试,看下效果:
完美,在需要填充内容的地方都已经动态设置上了内容。
四、多行数据如何生成?
如何做到动态生成多行呢?其实也很简单,重新打开刚刚修改的 xml 模板文件,在需要动态生成多行的地方,添加 freemark 循环表达式即可:
PS: 关于 Freemark 更多表达式的使用,小伙伴们可以自行在各大搜索引擎中搜索,因为如何使用 Freemark 不是本文关注的重点~
上图中,我们对后台的 commodities 字段做了循环,所以对应的,后台代码也需要做相关修改:
我们在 commodities 中添加了两个商品对象。赶快代码跑起来,看看效果!
别急,还有个地方需要做下修改,不然会报错!!
找到 <Table> 节点,有个属性叫 ExpandedRowCount, 它定义了表格行的总数,如果数值与实际的行数对应不上的话,会出问题。
这里我们添加 Freemark 表达式,总行数为商品 commodites 集合的大小加上 16, 注意:16 为除了动态生成的行数外,固定不变的行数大小,小伙伴们如果使用的是不同的 xml 模板,需要自行确认好这个数值的大小。
修改完了以后,再次运行单元测试,效果如下:
OK! 大功告成!
五、局限性
通过视图解析器来生成 Excel 的确很优雅(偷懒),同时兼具灵活性。但是它同样存在一些局限性!小伙伴们在技术选型时,需要结合实际的业务场景审视它是否适合。
- 版本问题:
 
目前个人测试结果是,在 MAC 系统上仅支持生成 03 版本 Excel, 07 版本存在打不开的情况;
- 无法写入大批量数据:
 
视图引擎生成文件无法往 Excel 里面追加数据,所以仅仅适用于数据量不大的个性化 Excel 生成,否则写入大批量数据时,存在内存溢出(OOM)的情况发生;
- MAC 系统存在生成的 Excel 文件无法编辑保存的情况:
 
小哈在测试中发现,生成 excel 在 MAC 系统上存在编辑后,无法保存的情况;而 Windows 系统 Microsoft Excel 和 WPS 均能够正常编辑保存;
六、总结
本文中,小哈给大家介绍了如何通过视图引擎优雅的生成 Excel 文件,演示了相关示例代码,以及它的相关局限性,希望大家看完本文后能够有所收获,下期见哟~
GitHub 示例代码
免费分享 | 面试&学习福利资源
最近在网上发现一个不错的 PDF 资源《Java 核心知识&面试.pdf》分享给大家,不光是面试,学习,你都值得拥有!!!
获取方式: 关注公众号: 小哈学Java, 后台回复资源,既可免费无套路获取资源链接,下面是目录以及部分截图:
重要的事情说两遍,关注公众号: 小哈学Java, 后台回复资源,既可免费无套路获取资源链接 !!!
如何通过 Freemark 优雅地生成那些花里胡哨的复杂样式 Excel 文件?的更多相关文章
- Python中生成(写入数据到)Excel文件
		
转自http://www.crifan.com/export_data_to_excel_file_in_python/ 在Python中,如何将数据,导出为Excel,即把数据写入到新生成的ex ...
 - 实现excel导入导出功能,excel导入数据到页面中,页面数据导出生成excel文件
		
今天接到项目中的一个功能,要实现excel的导入,导出功能.这个看起来思路比较清楚,但是做起了就遇到了不少问题. 不过核心的问题,大家也不会遇到了.每个项目前台页面,以及数据填充方式都不一样,不过大多 ...
 - 将excel中某列数据中,含有指定字符串的记录取出,并生成用这个字符串命名的txt文件
		
Python 一大重要的功能,就是可处理大量数据,那分不开的即是使用Excel表格了,这里我做下学习之后的总结,望对我,及广大同仁们是一个帮助Python处理Excel数据需要用到2个库:xlwt 和 ...
 - 阿里 EasyExcel 7 行代码优雅地实现 Excel 文件生成&下载功能
		
欢迎关注个人微信公众号: 小哈学Java, 文末分享阿里 P8 资深架构师吐血总结的 <Java 核心知识整理&面试.pdf>资源链接!! 个人网站: https://www.ex ...
 - springboot mail整合freemark实现动态生成模板
		
目标:1:springboot 整合 mail2: mail 使用freemark 实现模板动态生成(就是通过字符串生成模板,不需要在工程中写入固定模板)3: springboot 整合aop 实现日 ...
 - 用php生成一个excel文件(原理)
		
1.我们用php来生成一个excel文档来讲述其原理: excel2007里面的文档目录组成部分为: 2.我们使用ZipArchive()方法来生成一个简易的excel文件. 使用方法: 3.代码如下 ...
 - 动态生成一个设定好特殊样式的Tlabel,快速生成代码
		
动态生成一个设定好特殊样式的Tlabel,快速生成代码: 1.自己先在可视化界面设定一个Label,像这样: 2.选择label,快捷键ctrl+C 复制,粘贴带代码编辑器去,会生成一段这样的窗体代码 ...
 - Java生成和操作Excel文件(转载)
		
Java生成和操作Excel文件 JAVA EXCEL API:是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容.创建新的Excel文件.更新已经存在的Excel文件.使用该A ...
 - Java生成和操作Excel文件
		
JAVA EXCEL API:是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容.创建新的Excel文件.更新已经存在的Excel文件.使用该API非Windows操作系统也可以通过 ...
 
随机推荐
- WCF通信简单学习实例
			
最近在学习WCF通信,自己简单做个实例分享一下,环境是VS2015,使用的项目都是WPF的项目,其实大家用Winform或者Web项目也可以,都可以用的. 一.服务器端 1.创建WCF服务 服务名为W ...
 - vs未能正确加载CSharpPackage包,未能正确加载“Microsoft.VisualStudio.Editor.Implementation.EditorPackage”包
			
VS2017打开项目时提示未能正确加载CSharpPackage包, 可以使用 devenv命令工具来解决,操作如下 打开vs2017开发人员命令提示符(请使用管理员身份运行),如图 敲入 deve ...
 - JQ中的Ajax的封装
			
1.认识JQ中ajax的封装 jQ 对于ajax的封装有两层实现:$.ajax 为底层封装实现:基于 $.ajax ,分别实现了$.get 与$.post 的高层封装实现: 2.Ajax的底 ...
 - mssql下调用dll
			
1.新建类库,里面写入方法,类库里必须样静态,防止实例化 2.将dll存放到文件夹里 3.在sql中运行 EXEC sp_configure 'clr enabled' , '1'; --0代表不允许 ...
 - tornado中传递参数的几种方式
			
方法一 :tornado路由可以使用正则表达式中的子表达式传递url参数.比如:(r"/member//(\w*)/([01]*)", MemberHandler)匹配以后,tor ...
 - u盘 安装 centOS 7
			
实际上, 对于服务器的安装, 最好是能够远程批量安装(可能有些难度, 不是专业运维) 镜像下载地址: http://59.80.44.49/isoredirect.centos.org/centos/ ...
 - 为Dynamics CRM注释的图片附件做个预览功能
			
关注本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复163或者20151017可方便获取本文,同时可以在第一时间得到我发布的最新的博文信息,follow me! Dynamics CRM中注释可 ...
 - Wireshark小技巧:将IP显示为域名
			
" 本文介绍如何使Wireshark报文窗口的Source栏及Destination内的IP直接显示为域名,提升报文分析效率." 之前内容发现部分不够严谨的地方,所以删除重发. ...
 - 【LeetCode】53.最大子序和
			
最大子序和 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和. 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: ...
 - HOW TO: Setting up Encrypted Communications Channels in Oracle Databas
			
access_timeSeptember 22, 2015 person_outlineMartin Rakhmanov share In this article, I will explain h ...