# freemarker

back

FreeMarker是一个用Java语言编写的模板引擎,它基于模板来生成文本输出。FreeMarker与Web容器无关,即在Web运行时,它并不知道Servlet或HTTP。它不仅可以用作表现层的实现技术,而且还可以用于生成XML,JSP或Java 等。
这是一个相当老牌的开源的免费的模版引擎。通过 Freemarker 模版,我们可以将数据渲染成 HTML 网页、电子邮件、配置文件以及源代码等。Freemarker 不是面向最终用户的,而是一个 Java 类库,我们可以将之作为一个普通的组件嵌入到我们的产品中。
Freemarker 模版后缀为 .ftl(FreeMarker Template Language)。FTL 是一种简单的、专用的语言,它不是像 Java 那样成熟的编程语言。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

# 内建函数

return | 参考地址

含义 函数
获取当前时间 ${.now}

# 语法

return

图片要显示,需放在div中

注意:使用freemaker,要求所有标签必须闭合,否则会导致freemaker无法解析。
freemaker注释:<#-- 注释内容 -->格式部分,不会输出

# 基础语法

back

  • 1、字符输出
语法 含义
${emp.name?if_exists} 变量存在,输出该变量,否则不输出
${emp.name!} 变量存在,输出该变量,否则不输出
${emp.name?default("xxx")} 变量不存在,取默认值xxx
${emp.name!"xxx"} 变量不存在,取默认值xxx
常用内部函数
${"123<br>456"?html} 对字符串进行HTML编码,对html中特殊字符进行转义
${"str"?cap_first} 使字符串第一个字母大写subs
${"Str"?lower_case} 将字符串转换成小写
${"Str"?upper_case} 将字符串转换成大写
${"str"?trim} 去掉字符串前后的空白字符
字符串的两种拼接方式拼接
${"hello${emp.name!}"} 输出hello+变量名
${"hello"+emp.name!} 使用+号来连接,输出hello+变量名
可以通过如下语法来截取子串
<#assign str = "abcdefghijklmn"/> 定义变量str
${str?substring(0,4)} 输出abcd
${str[0]}${str[4]} 结果是ae
${str[1..4]} 结果是bcde
${str?index_of("n")} 返回指定字符的索引
  • 2、日期输出 date、time、datetime--><#assign test1 = "2009-01-22"?date("yyyy-MM-dd") />
    ${emp.date?string('yyyy-MM-dd')}-->日期格式
    <font color="red"> ${date?string('yyyy-MM-dd HH:mm:ss')} </font>

  • ${var?html}

转义HTML内容

  • 3、数字输出(以数字20为例)
语法 含义
${emp.name?string.number} 输出20
${emp.name?string.currency} ¥20.00
${emp.name?string.percent} 20%
${1.222?int} 将小数转为int,输出1
<#setting number_format="percent"/> 设置数字默认输出方式('percent',百分比)
<#assign answer=42/> 声明变量 answer 42
#{answer} 输出 4,200%
${answer?string} 输出 4,200%
${answer?string.number} 输出 42
${answer?string.currency} 输出 ¥42.00
${answer?string.percent} 输出 4,200%
#{answer} 输出 42

数字格式化插值可采用#{expr;format}形式来格式化数字,其中format可以是:
mX:小数部分最小X位
MX:小数部分最大X位

如下面的例子:

<#assign x=2.582/><#assign y=4/>
#{x; M2} // 输出2.58
#{y; M2} // 输出4
#{x; m2} // 输出2.58
#{y; m2} // 输出4.0
#{x; m1M2} // 输出2.58
#{y; m1M2} // 输出4.0

  • 4、申明变量

<#assign foo=false/>// 声明变量,插入布尔值进行显示,注意不要用引号
${foo?string("yes","no")} // 当为true时输出"yes",否则输出"no"

申明变量的几种方式

<#assign name=value>
<#assign name1=value1 name2=value2 ... nameN=valueN>
<#assign same as above... in namespacehash>
<#assign name> capture this </#assign>
<#assign name in namespacehash> capture this </#assign>

  • 5、比较运算符

表达式中支持的比较运算符有如下几个:
=== :判断两个值是否相等.
!= :判断两个值是否不等.
>gt :判断左边值是否大于右边值
>=gte :判断左边值是否大于等于右边值
<lt :判断左边值是否小于右边值
<=lte :判断左边值是否小于等于右边值

  • 6、算术运算符

FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:
+, - , - , / , % 注意:
(1)、运算符两边必须是数字
(2)、使用+运算符时,如果一边是数字,一边是字符串,就会自动将数字转换为字符串再连接,如:${3 + "5"},结果是:35

  • 7、逻辑运算符

逻辑运算符有如下几个:
逻辑与:&&
逻辑或:||
逻辑非:!
逻辑运算符只能作用于布尔值,否则将产生错误

  • 8、FreeMarker中的运算符优先级如下(由高到低排列):

①、一元运算符:!
②、内建函数:?
③、乘除法:*, / , %
④、加减法:- , +
⑤、比较:> , < , >= , <= (lt , lte , gt , gte)
⑥、相等:== , = , !=
⑦、逻辑与:&&
⑧、逻辑或:||
⑨、数字范围:..
实际上,我们在开发过程中应该使用括号来严格区分,这样的可读性好,出错少

  • 9、if 逻辑判断(注意:elseif 不加空格)
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
<#else>
...
</#if>
1
2
3
4
5
6
7
8
9

if 空值判断 // 当 photoList 不为空时<#if photoList??>...</#if>
值得注意的是,${..}只能用于文本部分,不能用于表达式
下面的代码是错误的: <#if ${isBig}>Wow!</#if>
<#if "${isBig}">Wow!</#if>
正确写法--><#if isBig>Wow!</#if>

  • 10、switch (条件可为数字,可为字符串)
<#switch value>
 <#case refValue1>
 ....
<#break>
<#case refValue2>
....
<#break>
 <#case refValueN>
 ....
<#break>
 <#default>
 ....
 </#switch>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 11、集合 & 循环
  • 遍历集合:
<#list empList! as emp>
    ${emp.name!}
</#list>
1
2
3

!表示取值,可以省掉

  • 可以这样遍历集合:
<#list 0..(empList!?size-1) as i>
    ${empList[i].name!}
</#list>
1
2
3

与jstl循环类似,也可以访问循环的状态。

empList?size:    // 取集合的长度
emp_index:     // int类型,当前对象的索引值
emp_has_next: // boolean类型,是否存在下一个对象

索引问题

<#list empList[4..6] as emp> ${emp_index} </#list>

循环输出的index值为:0,1,2

使用<#break>跳出循环

<#if emp_index = 0><#break></#if>

集合长度判断

<#if empList?size != 0></#if> // 判断=的时候,注意只要一个=符号,而不是==
<#assign l=0..100/> // 定义一个int区间的0~100的集合,数字范围也支持反递增,如100..2

<#list 0..100 as i>  // 等效于java for(int i=0; i <= 100; i++)
  ${i}
</#list>
1
2
3

截取子集合:

empList[3..5] //返回empList集合的子集合,子集合中的元素是empList集合中的第4-6个元素

创建集合:

<#list ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"] as x>

集合连接运算,将两个集合连接成一个新的集合

<#list ["星期一","星期二","星期三"] + ["星期四","星期五","星期六","星期天"] as x>

除此之外,集合元素也可以是表达式,例子如下:

[2 + 2, [1, 2, 3, 4], "whatnot"]

seq_contains:判断序列中的元素是否存在

<#assign x = ["red", 16, "blue", "cyan"]>
${x?seq_contains("blue")?string("yes", "no")}    // yes
${x?seq_contains("yellow")?string("yes", "no")}  // no
${x?seq_contains(16)?string("yes", "no")}        // yes
${x?seq_contains("16")?string("yes", "no")}      // no
1
2
3
4
5

seq_index_of:第一次出现的索引

<#assign x = ["red", 16, "blue", "cyan", "blue"]>
${x?seq_index_of("blue")}  // 2
1
2

sort_by:排序(升序)

<#list movies?sort_by("showtime") as movie></#list>
1

sort_by:排序(降序)

<#list movies?sort_by("showtime")?reverse as movie></#list>
1

具体介绍:
不排序的情况:

<#list movies as moive>
  <a href="${moive.url}">${moive.name}</a>
</#list>
//要是排序,则用
<#list movies?sort as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>

// 这是按元素的首字母排序。若要按list中对象元素的某一属性排序的话,则用
<#list moives?sort_by(["name"]) as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>

//这个是按list中对象元素的[name]属性排序的,是升序,如果需要降序的话,如下所示:
<#list movies?sort_by(["name"])?reverse as movie>
  <a href="${movie.url}">${movie.name}</a>
</#list>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 12、Map对象
// 创建map
<#assign scores = {"语文":86,"数学":78}>

// Map连接运算符
<#assign scores = {"语文":86,"数学":78} + {"数学":87,"Java":93}>

// Map元素输出
emp.name       // 全部使用点语法
emp["name"]    // 使用方括号
1
2
3
4
5
6
7
8
9
  • 13、FreeMarker支持如下转义字符:
转义字符 含义
\" 双引号(u0022)
\' 单引号(u0027)
\\ 反斜杠(u005C)
\n 换行(u000A)
\r 回车(u000D)
\t Tab(u0009)
\b 退格键(u0008)
\f Form feed(u000C)
\l <
\g >
\a &
\{ {
\xCode 直接通过4位的16进制数来指定Unicode码,输出该unicode码对应的字符.

如果某段文本中包含大量的特殊符号,FreeMarker提供了另一种特殊格式:
可以在指定字符串内容的引号前增加r标记,在r标记后的文件将会直接输出.看如下代码:

${r"${foo}"}    // 输出 ${foo}
${r"C:/foo/bar"}    // 输出 C:/foo/bar
1
2
  • 14、include指令
// include指令的作用类似于JSP的包含指令:
<#include "/test.ftl" encoding="UTF-8" parse=true>

// 在上面的语法格式中,两个参数的解释如下:
encoding="GBK"  // 编码格式
parse=true    // 是否作为ftl语法解析,默认是true,false就是以文本方式引入,
1
2
3
4
5
6

注意:在ftl文件里布尔值都是直接赋值的如parse=true,而不是parse="true"

  • 15、import指令
// 类似于jsp里的import,它导入文件,然后就可以在当前文件里使用被导入文件里的宏组件
<#import "/libs/mylib.ftl" as my>
// 上面的代码将导入/lib/common.ftl模板文件中的所有变量,
1
2
3

交将这些变量放置在一个名为com的Map对象中,"my"在freemarker里被称作namespace

  • 16、compress 压缩
// 用来压缩空白空间和空白的行
<#compress>
    ...
</#compress>
<#t> // 去掉左右空白和回车换行
<#lt>// 去掉左边空白和回车换行
<#rt>// 去掉右边空白和回车换行
<#nt>// 取消上面的效果
1
2
3
4
5
6
7
8
  • 17、escape,noescape 对字符串进行HTML编码

// escape指令导致body区的插值都会被自动加上escape表达式,但不会影响字符串内的插值,只会影响到body内出现的插值,
使用escape指令的语法格式如下:

<#escape x as x?html>
  First name: ${firstName}
<#noescape>Last name: ${lastName}</#noescape>
  Maiden name: ${maidenName}
</#escape>

// 相同表达式
First name: ${firstName?html}
Last name: ${lastName}
Maiden name: ${maidenName?html}
1
2
3
4
5
6
7
8
9
10

# 高级语法

back

  • 1、global全局赋值语法
<#global name=value>
<#global name1=value1 name2=value2 ... nameN=valueN>

<#global name>
  capture this
</#global>
// 利用这个语法给变量赋值,那么这个变量在所有的namespace中是可见的,
1
2
3
4
5
6
7

如果这个变量被当前的assign语法覆盖如<#global x=2><#assign x=1>在当前页面里x=2将被隐藏,或者通过${.globals.x} 来访问

  • 2、setting 语法
// 用来设置整个系统的一个环境  
locale // zh_CN 中文环境  
number_format
boolean_format
date_format , time_format , datetime_format
time_zone
classic_compatible
// 例1:
<#setting number_format="percent"/>    // 设置数字默认输出方式('percent',百分比)

// 例2:
// 假如当前是匈牙利的设置,然后修改成美国
${1.2} // 输出1,2
<#setting locale="en_US">
${1.2} // 输出1.2,因为匈牙利是采用", "作为十进制的分隔符,美国是用". "
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 3、macro宏指令

例子1:

<#-- 定义宏 -->
<#macro test foo bar="Bar" baaz=-1>
  Text: ${foo}, ${bar}, ${baaz}
</#macro>

<#-- 使用宏 -->
<@test foo="a" bar="b" baaz=5*5/>  // 输出:Text: a, b, 25
<@test foo="a" bar="b"/>        // 输出:Text: a, b, -1
<@test foo="a" baaz=5*5-2/>     // 输出:Text: a, Bar, 23
<@test foo="a"/>                   // 输出:Text: a, Bar, -1
1
2
3
4
5
6
7
8
9
10

例子2:

<#-- 定义一个循环输出的宏 -->
<#macro list title items>
  ${title}
  <#list items as x>
    *${x}
  </#list>
</#macro>

<#-- 使用宏 -->
<@list items=["mouse", "elephant", "python"] title="Animals"/>
// 输出Animals *mouse *elephant *python
1
2
3
4
5
6
7
8
9
10
11

例子3:

<#-- 嵌套宏 -->
<#macro border>
  <table>
    <#nested>
  </table>
</#macro>

<#-- 嵌套宏使用 -->
<@border>
  <tr><td>hahaha</td></tr>
</@border>
1
2
3
4
5
6
7
8
9
10
11

输出结果:

<table>
  <tr><td>hahaha</td></tr>
</table>
1
2
3

例子4:在nested指令中使用循环变量时,可以使用多个循环变量,看如下代码:

<#-- 循环嵌套宏 --><#macro repeat count>  
<#list 1..count as x>
<#nested x, x/2, x==count>
 // 使用nested指令时指定了三个循环变量  
</#list></#macro><#-- 使用宏 -->
<@repeat count = 4; c, halfc, last>  
${c}. ${halfc}<#if last> Last!</#if></@repeat>
// 输出结果:// 1. 0.5// 2. 1// 3. 1.5// 4. 2 Last!
1
2
3
4
5
6
7
8

freemarker 宏嵌套nested 的使用:

  • 4、结束macro指令
// return指令用于结束macro指令
<#-- 创建宏 -->
<#macro book>
  spring
  <#return>
  j2ee
</#macro>

<#-- 使用宏 -->
<@book />
// 上面的代码输出:spring,而j2ee位于return指令之后,不会输出.
1
2
3
4
5
6
7
8
9
10
11

# 模板加载器

return

  • ClassTemplateLoader:从classpath中加载模板。
  • FileTemplateLoader:在文件系统中从指定文件夹加载模板。
  • StringTemplateLoader:从一个字符串 Map 中加载模板。
  • URLTemplateLoader:从 URL 中加载模板。 你必须要实现 getURL 方法。
  • WebappTemplateLoader:从 servlet 上下文中加载模板。
Configuration configuration = new Configuration();
configuration.setTemplateLoader(
new WebappTemplateLoader(servletContext, "WEB-INF/content"));
1
2
3

# 导出word

return

freemarker模板来生成word文档。

# 编写word模板

back

p

编写完成后,另存为xml文件类型,此处推荐使用vsCode(从未有过如此丝般顺滑的编辑器)来格式化生成的xml(需要安装插件XML Tools)文件

# 文字部分

back

需要替换的部分采用${}形式,编写完成后需要另存为xml文件,然后利用工具格式化xml文件,我这里采用vscode来进行xml文件的的格式化,得到如下文件:

<w:r>
    <w:rPr>
        <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>${</w:t>
</w:r>
<w:r>
    <w:t>depart</w:t>
</w:r>
<w:r>
    <w:rPr>
        <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>}</w:t>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

可以发现,部分${}被拆开了,需要我们手动将其中的部分删去,如下:

<w:rPr>
        <w:rFonts w:hint="eastAsia"/>
    </w:rPr>
    <w:t>${depart}</w:t>
</w:r>
1
2
3
4
5

此处建议一种方式来填写这些待替换的变量——通过excel,如下图:

p

然后将这些复制粘贴到word模板中对应的位置,即可实现大部分的${}不会被拆开

<w:tc>
    <w:tcPr>
        <w:tcW w:w="2074" w:type="dxa"/>
    </w:tcPr>
    <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
        <w:r w:rsidRPr="00952002">
            <w:t>${depart}</w:t>
        </w:r>
    </w:p>
</w:tc>
<w:tc>
    <w:tcPr>
        <w:tcW w:w="2074" w:type="dxa"/>
    </w:tcPr>
    <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
        <w:r w:rsidRPr="00952002">
            <w:t>${job}</w:t>
        </w:r>
    </w:p>
</w:tc>
<w:tc>
    <w:tcPr>
        <w:tcW w:w="2074" w:type="dxa"/>
    </w:tcPr>
    <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
        <w:r w:rsidRPr="00952002">
            <w:t>${name}</w:t>
        </w:r>
    </w:p>
</w:tc>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

对于替换量较大的模板更省时。

# 图片部分

back

可以看到xml中图片已被转换,搜索binaryData或者binData,可以发现一大段的编码,现将其替换成${}形式,具体名称自定,保持跟代码中一致即可

<pkg:binaryData>iVBORw0KGgoAAAANSUhEUgAAAPsAAAClCAYAAAHt8bu3AAAAAXNSR0IArs4c6QAAAARnQU1BAACx
jwv8YQUAAAAJcEhZcwAACxMAAAsTAQCanBgAABlvSURBVHhe7Z09rCRLdcdvTOIneQHdZ2E/gSVb
DtAzKZYgeAkSPFYW...
1
2
3
<pkg:binaryData>${image}</pkg:binaryData>
1

完成后保存并修改文件类型后缀为ftl,编码实现word导出

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 - 导出word类
 - @author huting
 */
public class ExportWordUtil {
    public static void main(String... args){
//        FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
        Configuration configuration = new Configuration();
        File outFile = new File("D:/JAVA_WORK/result.doc");
        try(Writer out = new BufferedWriter(
                new OutputStreamWriter(
                        new FileOutputStream(outFile),StandardCharsets.UTF_8),10204)) {
//            response.setContentType("octets/stream");
//            response.setHeader("content-Type", "application/msword");
//            String wordName="result";
//            response.setHeader("Content-Disposition", "attachment;filename="
//                    +new String( wordName.getBytes("gb2312"), "ISO8859-1" )
//                    +".doc");
            String rootTemplatePath = Optional.ofNullable(ResourceUtils.getURL("classpath:").getPath())
                    .orElse("D:/JAVA_WORK/MyTestSpringBoot/src/main/resources/");
//            configuration.setClassForTemplateLoading(ExportWordUtil.class,"/com");
            configuration.setDirectoryForTemplateLoading(new File(rootTemplatePath));
            Template template = configuration.getTemplate("templates/wordAlarm.ftl","utf-8");
            String imgFile = rootTemplatePath+"static/images/banner.jpg";
            Map<String,String> dataMap = generateDataMap();
            dataMap.put("image",transferImgToBase64(imgFile));
            template.process(dataMap,out);
        } catch (IOException | TemplateException e) {
            e.printStackTrace();
        }
    }

    private static String transferImgToBase64(String filePath) throws IOException {
        Base64 b64Encoder = new Base64();
        File file = new File(filePath);
        FileInputStream fis = new FileInputStream(file);
        byte[] imgByte = new byte[fis.available()];
        fis.read(imgByte);
        fis.close();
        return  b64Encoder.encodeAsString(imgByte);
    }

    private static Map<String, String> generateDataMap(){
        Map<String,String> map = new HashMap<>(10);
        map.put("title","某公司员工列表");
        map.put("depart","研发中心");
        map.put("job","攻城狮");
        map.put("name","某某");
        map.put("salary","¥-12,000.00");
        map.put("attention","快来围观");
        map.put("writer","某男");

        return map;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 带循环的导出

back

还是刚才的word模板,查找w:tr,第一对是标题的,找到第二对的w:tr,做处理;

<#list listDataMap as dataMap>
<w:tr w:rsidR="007E690B" w:rsidTr="007E690B">
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="2074" w:type="dxa"/>
        </w:tcPr>
        <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
            <w:r w:rsidRPr="00952002">
                <w:t>${dataMap.depart}</w:t>
            </w:r>
        </w:p>
    </w:tc>
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="2074" w:type="dxa"/>
        </w:tcPr>
        <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
            <w:r w:rsidRPr="00952002">
                <w:t>${dataMap.job}</w:t>
            </w:r>
        </w:p>
    </w:tc>
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="2074" w:type="dxa"/>
        </w:tcPr>
        <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
            <w:r w:rsidRPr="00952002">
                <w:t>${dataMap.name}</w:t>
            </w:r>
        </w:p>
    </w:tc>
    <w:tc>
        <w:tcPr>
            <w:tcW w:w="2074" w:type="dxa"/>
        </w:tcPr>
        <w:p w:rsidR="007E690B" w:rsidRDefault="00952002" w:rsidP="007E690B">
            <w:r w:rsidRPr="00952002">
                <w:t>${dataMap.salary}</w:t>
            </w:r>
        </w:p>
    </w:tc>
</w:tr>
</#list>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

编码如下:

public class ExportWordUtil {
    public static void main(String... args){
//        FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
        Configuration configuration = new Configuration();
        File outFile = new File("D:/JAVA_WORK/result.doc");
        try(Writer out = new BufferedWriter(
                new OutputStreamWriter(
                        new FileOutputStream(outFile),StandardCharsets.UTF_8),10204)) {
//            response.setContentType("octets/stream");
//            response.setHeader("content-Type", "application/msword");
//            String wordName="result";
//            response.setHeader("Content-Disposition", "attachment;filename="
//                    +new String( wordName.getBytes("gb2312"), "ISO8859-1" )
//                    +".doc");
//            template.process(root, new BufferedWriter(new OutputStreamWriter(response.getOutputStream(),"utf-8"),10240));
            String rootTemplatePath = Optional.ofNullable(ResourceUtils.getURL("classpath:").getPath())
                    .orElse("D:/JAVA_WORK/MyTestSpringBoot/src/main/resources/");
//            configuration.setClassForTemplateLoading(ExportWordUtil.class,"/com");
            configuration.setDirectoryForTemplateLoading(new File(rootTemplatePath));
            Template template = configuration.getTemplate("templates/wordAlarm.ftl","utf-8");
            String imgFile = rootTemplatePath+"static/images/banner.jpg";
            Map<String,Object> dataMap = new HashMap<>(2);
            dataMap.put("listDataMap",generateListDataMap(8));
            dataMap.put("image",transferImgToBase64(imgFile));
            dataMap.put("title","某公司员工列表");
            dataMap.put("attention","快来围观");
            dataMap.put("writer","某男");
            template.process(dataMap,out);
        } catch (IOException | TemplateException e) {
            e.printStackTrace();
        }
    }

    private static String transferImgToBase64(String filePath) throws IOException {
        Base64 b64Encoder = new Base64();
        File file = new File(filePath);
        FileInputStream fis = new FileInputStream(file);
        byte[] imgByte = new byte[fis.available()];
        fis.read(imgByte);
        fis.close();
        return  b64Encoder.encodeAsString(imgByte);
    }

    private static Map<String, String> generateDataMap(int i){
        Map<String,String> map = new HashMap<>(10);
        map.put("depart","研发中心");
        map.put("job","攻城狮"+i);
        map.put("name","某某"+i);
        map.put("salary","¥-12,000.00");

        return map;
    }

    private static List<Map<String, String>> generateListDataMap(Integer length){
        List<Map<String,String>> lists = new ArrayList<>();
        for (int i = 1; i < Optional.ofNullable(length).filter(len->len<=0).orElse(6); i++){
            lists.add(generateDataMap(i));
        }
        return lists;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 导出excel

ToTop

类似word,制作好excel文件,另存为xml,调整需要调整的xml文件中的位置,进行动态替换即可。

# freemarker与sping整合

ToTop

@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;

Template template = freeMarkerConfigurer.getConfiguration().getTemplate("wordAlarm.ftl","utf-8");
1
2
3
4

# 整合springboot

ToTop

pom文件

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
<!--            <version>2.1.6.RELEASE</version>-->
        </dependency>
1
2
3
4
5

可以看到freemarker的自动配置

package org.springframework.boot.autoconfigure.freemarker;

@Configuration
@ConditionalOnClass({ freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class })
@EnableConfigurationProperties(FreeMarkerProperties.class)
@Import({ FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class,
    FreeMarkerNonWebConfiguration.class })
public class FreeMarkerAutoConfiguration {

private static final Log logger = LogFactory.getLog(FreeMarkerAutoConfiguration.class);

private final ApplicationContext applicationContext;

private final FreeMarkerProperties properties;

public FreeMarkerAutoConfiguration(ApplicationContext applicationContext, FreeMarkerProperties properties) {
    this.applicationContext = applicationContext;
    this.properties = properties;
}

@PostConstruct
public void checkTemplateLocationExists() {
    if (logger.isWarnEnabled() && this.properties.isCheckTemplateLocation()) {
        List<TemplateLocation> locations = getLocations();
        if (locations.stream().noneMatch(this::locationExists)) {
            logger.warn("Cannot find template location(s): " + locations + " (please add some templates, "
                    + "check your FreeMarker configuration, or set "
                    + "spring.freemarker.checkTemplateLocation=false)");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

从这里可以看出,当 classpath 下存在 freemarker.template.Configuration 以及 FreeMarkerConfigurationFactory 时,配置才会生效,也就是说当我们引入了 Freemarker 之后,配置就会生效。但是这里的自动化配置只做了模板位置检查,其他配置则是在导入的 FreeMarkerServletWebConfiguration 配置中完成的。那么我们再来看看

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, FreeMarkerConfigurer.class })
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration {

protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) {
    super(properties);
}

@Bean
@ConditionalOnMissingBean(FreeMarkerConfig.class)
public FreeMarkerConfigurer freeMarkerConfigurer() {
    FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
    applyProperties(configurer);
    return configurer;
}

@Bean
public freemarker.template.Configuration freeMarkerConfiguration(FreeMarkerConfig configurer) {
    return configurer.getConfiguration();
}

@Bean
@ConditionalOnMissingBean(name = "freeMarkerViewResolver")
@ConditionalOnProperty(name = "spring.freemarker.enabled", matchIfMissing = true)
public FreeMarkerViewResolver freeMarkerViewResolver() {
    FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
    getProperties().applyToMvcViewResolver(resolver);
    return resolver;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

我们来简单看下这段源码:

@ConditionalOnWebApplication 表示当前配置在 web 环境下才会生效
ConditionalOnClass 表示当前配置在存在 Servlet 和 FreeMarkerConfigurer 时才会生效。
@AutoConfigureAfter 表示当前自动化配置在 WebMvcAutoConfiguration 之后完成。

代码中,主要提供了 FreeMarkerConfigurer 和 FreeMarkerViewResolver。
FreeMarkerConfigurer 是 Freemarker 的一些基本配置,例如 templateLoaderPath、defaultEncoding 等

FreeMarkerViewResolver 则是视图解析器的基本配置,包含了viewClass、suffix、allowRequestOverride、allowSessionOverride 等属性。

构造方法中配置了基本信息

@ConfigurationProperties(prefix = "spring.freemarker")
public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {

public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";

public static final String DEFAULT_PREFIX = "";

public static final String DEFAULT_SUFFIX = ".ftl";

/**
    - Well-known FreeMarker keys which are passed to FreeMarker's Configuration.
    */
private Map<String, String> settings = new HashMap<>();java
1
2
3
4
5
6
7
8
9
10
11
12
13

如需要修改配置文件:

spring.freemarker.allow-request-override=false
spring.freemarker.allow-session-override=false
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.suffix=.ftl
spring.freemarker.template-loader-path=classpath:/templates/
1
2
3
4
5
6
7
8
9
10

配置文件按照顺序依次解释如下:

HttpServletRequest的属性是否可以覆盖controller中model的同名项
HttpSession的属性是否可以覆盖controller中model的同名项
是否开启缓存
模板文件编码
是否检查模板位置
Content-Type的值
是否将HttpServletRequest中的属性添加到Model中
是否将HttpSession中的属性添加到Model中
模板文件后缀
模板文件位置