在web或其他应用中,经常我们需要导出或者预览word文档,比较实际的例子有招聘网站上预览或者导出个人简历,使用POI导出excel会非常的方便,但是如果想导出word,由于其格式控制非常复杂,故而使用POI将会非常麻烦,而FreeMarker则可以较好的解决这个问题;并且,根据FreeMarker的实现原理,预览word也会变得非常简单。
FreeMarker主要有三个部分:模板,数据源以及数据的存储。可想而知,在导出word的时候,我们必须得告诉FreeMarker我们需要导出的word的格式以及将要填充到这个word中的数据,因而模板和数据源是我们需要准备的部分。这里需要另外说明的是,FreeMarker关心的不是模板文件的类型或具体内容,其关心的是模板文件中的ftl标签和其中获取数据的表达式(这部分将在后续进行讲解)。FreeMarker的强大之处也就在这个位置,这里的模板可以是任意类型的模板,而数据源由我们按照指定的格式封装即可。那么也就是说,对于预览操作,我们如果事先制作一个html模板,点击预览后由FreeMarker向按照该模板向新建的html文件中填充数据,接着在前台js中新开窗口将该html文件(填充数据后即为一个静态页面)显示出来即可达到预览的效果。
这里我们以word文件的导出为例来讲解FreeMarker的使用,我们使用的IDE为Intellij IDEA,框架为springboot,项目是使用Maven构建的。
模板文件的创建可以使用word2007及以上版本完成,首先我们创建一个如下格式的word文档:
创建后将该文件以xml格式存储
使用xml文本编辑器打开该xml文件,检查其中的取值表达式是否发生格式错误,如果发生格式错误就将其中错误的部分删除,使其恢复我们填写的格式(格式错误一般会发生在取值表达式中含有特殊字符的时候)。
如图中所示,${user.password}就发生了格式错误,我们将中间错误部分删除后如下:
接着将该模板文件另存为UserList.ftl,文件格式为全部文件:
将该文件复制到项目中,打开并格式化,找到其中${user.username}和${user.password},仔细分析该ftl文件可以发现,在word文档中表格的每一行在xml文件中即为一个<w:tr></w:tr>标签,而在该标签中,每一个<w:tc></w:tc>则对应一个单元格。了解这个之后,我们就要使用ftl语法对该模板文件进行改造。这里我们需要导出的是一个用户列表的文件,每个用户包含一个用户名和密码,那么这里用户所在的这一行(<w:tr></w:tr>)就需要使用ftl中的<#list></#list>标签包含起来:
<w:tr>
<w:tc>
...
用户名:
...
</w:tc>
<w:tc>
...
${user.username}
...
</w:tc>
<w:tc>
...
密码:
...
</w:tc>
<w:tc>
...
${user.password}
...
</w:tc>
</w:tr>
</#list>
到此为止,我们的ftl模板就制作完毕了。接下来我们创建后台服务端的代码,实体类创建如下:
public class User { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public User() { } public User(String username, String password) { this.username = username; this.password = password; } }
Controller层创建如下:
import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/word") public class WordController { @Autowired private WordService wordService; @RequestMapping(value = "/createUserListWord", method = RequestMethod.GET) public ResponseEntity<Void> createUserListWord() { wordService.createUserListWord(); return ResponseEntity.ok().build(); } }Service层的接口及其实现类如下:
public interface WordService { void createUserListWord(); }
import javax.transaction.Transactional; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.stereotype.Service; @Service public class WordServiceImpl implements WordService { public void createUserListWord() { Map<?, ?> root = initData(); //数据源对象 String template = "/template/UserList.ftl"; //模板文件的地址 String path = "E:\UserList.doc"; //生成的word文档的输出地址 WordUtil.process(root, template, path); } private Map<?, ?> initData() { Map<String, Object> root = new HashMap<String, Object>(); List<User> users = new ArrayList<User>(); User zhangsan = new User("张三", "123"); User lisi = new User("李四", "456"); User wangwu = new User("王五", "789"); users.add(zhangsan); users.add(lisi); users.add(wangwu); root.put("users", users); root.put("title", "用户列表"); return root; } }这里需要说明的一点是,在FreeMarker中,数据一般是以Map,List以及实体类对象的形式存储,这里数据的初始化函数中则将三种形式的数据存储方式都用到了。在模板中取值的时候,对于Map对象中的数据,使用${key}即可获取,这里key表示Map中的键,对于List,则可以使用下标的方式,也可以使用循环的方式,这里我们是将User对象存储于List中,在模板中则可以使用users[i]来获取List中第i个User对象,如users[i].username;也可以使用循环来对List集合进行遍历,如
<#list users as user> ${user.username} </#list>这里users表示存储List的Map的key的值。
最后则是FreeMarker中生成word文档的核心函数:
import freemarker.template.Configuration; import freemarker.template.Template; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; public final class WordUtil { private static Configuration configuration = null; private WordUtil() { throw new AssertionError(); } /** * 根据模板生成相应的文件 * @param root 保存数据的map * @param template 模板文件的地址 * @param path 生成的word文档输出地址 * @return */ public static synchronized File process(Map<?, ?> root, String template, String path) { if (null == root ) { throw new RuntimeException("数据不能为空"); } if (null == template) { throw new RuntimeException("模板文件不能为空"); } if (null == path) { throw new RuntimeException("输出路径不能为空"); } File file = new File(path); String templatePath = template.substring(0, template.lastIndexOf("/")); String templateName = template.substring(template.lastIndexOf("/") + 1, template.length()); if (null == configuration) { configuration = new Configuration(Configuration.VERSION_2_3_23); // 这里Configurantion对象不能有两个,否则多线程访问会报错 configuration.setDefaultEncoding("utf-8"); configuration.setClassicCompatible(true); } configuration.setClassForTemplateLoading(WordUtil.class, templatePath); Template t = null; try { t = configuration.getTemplate(templateName); Writer w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "utf-8")); t.process(root, w); // 这里w是一个输出地址,可以输出到任何位置,如控制台,网页等 w.close(); } catch (Exception e) { throw new RuntimeException(e); } return file; } }至此,使用FreeMarker生成word文档的核心代码已经全部书写完毕,最后打开浏览器访问http://localhost:8081/word/createUserListWord,在E盘根目录下就会生成一个word文档,其内容如下:
这只是FreeMarker的一个简单应用,关于FreeMarker生成html文件,这里有一点需要说明,word2007及以上版本保存文件的格式可以为xml文件,也可以为html文件,检查该html文件中取值表达式的格式无误之后按照上述步骤也可以生成我们需要的html文件,但是生成的html文件的格式不一定是我们需要的格式,并且在修改ftl模板的时候,由于模板中标签元素的样式等较多,因而修改较为复杂。若想达到预览的效果,我们可以不使用上述方法生成的html模板,而是手动书写一份格式一致的html文件,然后保存为ftl格式模板,这个过程并不复杂,并且可读性较强。