SpringMVC:ViewResolver:FreeMarker:类型 / (内置&自定义)函数 / 宏 / include&import

更多
2021年07月27日 19点08分 作者:叶飞 修改

ViewResolver

实际开发中,jsp文件放在WEB-INF目录下,因为这个目录下的内容不能直接通过request请求得到,更加安全。

演示:直接请求webapp下.jsp文件

另外我们也喜欢把所有的.jsp文件放在统一的views文件夹下。

这样,我们的ModelAndView()中的路径就显得更加累赘了……

InternalResourceViewResolver

可以在springmvc-servlet.xml进行配置:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<property name="prefix" value="/WEB-INF/views/" />
	<property name="suffix" value=".jsp" />
</bean>	

InternalResourceViewResolver是一个类,SpringMVC默认使用这个类在解析(resolve).jsp视图(view)。

我们这里为添加了两个自定义规则,修正ModelAndView()中的参数:

  • 添加前缀(prefix):/WEB-INF/views/
  • 添加后缀(suffix):.jsp

所以我们所有的handler method可以这样返回:


模板:FreeMarker

大家觉得JSP的语法怎么样?事实上,更多的时候我们更愿意使用一些更强大更优雅的“模板”来代替.jsp,比如freemarker。

maven下载

freemarker自己的包:

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
     <version>2.3.30</version> <!-- 最新版 2.3.31 -->
 </dependency>

以及,嵌入SpringMVC需要的:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

新建.ftl文件

也可以使用其他后缀名,但

  • 首先这是惯例,
  • 其次使用.ftl后缀可以有插件的智能提示的支持:(按提示安装即可)

配置ViewResolver

最后,告诉SpringMVC使用FreeMarkerViewResolver解析

<bean
	class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
	<property name="suffix" value=".ftl" />
</bean>

演示:FreeMarkerViewResolver实现

#体会:多态的作用,可插拔式设计(包括JDBC、Hibernate……)

FreeMarkerViewResolver不要加前缀,而是要在FreeMarkerConfigurer通过配置templateLoaderPath指定ftl模板所在的位置:

<bean
	class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
	<property name="templateLoaderPath" value="/WEB-INF/views" />
</bean>
上述配置都显式指定了编码格式,使用utf-8解决中文乱码的问题。


相对来说,FreeMarker比JSP更为复杂,因为它几乎创建了一个专门的语法:FTL (FreeMarker Template Language)

Tips:

  • 因为FreeMarker的语法会和HTML规则冲突,建议在windows - preferences - validation 中关闭 HTML 校验
  • 如果HTML代码的高亮提示等,在打开文件(open with)的时候,还可以选择HTML Editor:

数据类型

局部变量的赋值使用<#assgin>声明
<#assign name="飞哥">

也可以一次声明多个:

<#assign name="fg" age=23>

对同一变量的再次赋值会覆盖/修改之前的值。(演示:${name}

注意这里已经没有类型声明了,但FTL还是有类型的(类似于弱类型语言仍然有类型),包括:

  • 标量:单个数据,又包括:
    • 字符串,如上所示
    • 数字,不区分整数小数……
      <#assign weight = 76.5>
      <#assign age = 23>
      <#assign credit = -100>
    • 布尔值
      <#assign isMale = true>
    • 时间,.now代表当前时间
      <#assign enroll = .now>
  • 集合:或者“容器”,又包括:
    • 序列:按存储位置(下标)有序组织
      <#assign students=["atai", "bo", "lang"]>
      <#assign numbers=[1, 2, 3, 4, 5]>
      <#--可简写成:-->
      <#assign numbers=1..5>
    • 哈希表:键值对
      <#assign students={"atai":"阿泰", "bo":"波仔", "lang":"浪神"}>


表达式

标量

字符串、数字和日期都可以直接表达:

姓名:${name} <br />
体重:${weight} 年龄:${age} 积分: ${credit} <br />
报到时间:${enroll}
时间可以格式化:
报到日期:${enroll?string("yyyy年MM月dd日")} <br />
报到时间:${enroll?string("HH点mm分ss秒")} <br />

但bool值不行(一般也不会直接输出)

性别:${isMale?string("男","女")} <br />
确实要输出true和false,用:
性别:${isMale?string}  或者: <br />
性别:${isMale?c} 

序列集合

用下标取单个元素值:
${numbers[0]}
循环取值,使用关键字 list 和 as:
<#list numbers as n>
	${n}-
</#list>
还可以用下标(从0开始)进行截取
<#list numbers[2..4] as n>

这个语法用于截取字符串非常方便:(由此可见,FTL把字符串当成了集合)

<#assign greet="一起帮·源栈欢迎你">
${greet[0..5]}

哈希表

按键(key)取单个元素的值:

${students["atai"]}

遍历要首先经过转换

<#list students?keys[1..2] as n> 或者
<#list students?values[1..2] as n>


运算

和Java非常类似,也包括

数值运算

3 + 2 = ${3 + 2} <br />
3 + 2.0 = ${3 + 2.0} <br />
3 / 2 = ${3 / 2} <br />
3 % 2 = ${3 % 2} <br />
3 % 2.0 = ${3 % 2.0} <br />
3 % 1.5 = ${3 % 1.5} <br />
3 % 1.1 = ${3 % 1.1} <br />
区别:没有整数除整数结果仍为整数的要求,包含小数之后取余有点墨幻

比较运算

因为<和>容易和HTML标签混淆,所以TPL引入了以下别名:

  • gt:greater than,>
  • lt:less than,<
  • gte:greater than or equal,>=
  • lte:less than,<=

可以直接比较时间,好评!

${(.now>.now)?c}
${(.now==.now)?c}
${(.now<.now)?c}
${(.now!=.now)?c}
不同类型直接的数据不能比较(比JavaScript好)

逻辑运算

和Java完全一致:或(||)且(&&)取反(!)


注释

<#-- <#assign name="飞哥"> -->
<#-- ${name} -->
<#-- <input type="text"> --> 
居然没有快捷键,o(╥﹏╥)o


分支

仍然使用if...else,
<#if .now gt .now>
	<p>.now gt .now</p>
<#else>	
	<p>.now NOT gt .now</p>
</#if>
一样可以嵌套:
<#if .now gt .now>
	<#if false>
		<p>.now gt .now</p>
	</#if>
<#else>	
但是else if中间没有空格要连起来写:
<#elseif 3 lt 2>

空值判断

FTL里面没有关键字null,所以这样的写法是会报错的:

<#if name==null>

要判断一个变量是不是为null(unassigned/undefined),要使用双问号(??)

<#if name??>
	<h2>${name}</h2>
<#else>
	<h2>匿名</h2>
</#if>

还可以直接给null值变量

${name!"匿名用户"}
${name!0}
${name!""}


内置函数

FTL中使用函数有两个特点(和Java的不同点):

  • 用问号(?)代替点(.)
  • 如果函数没有参数,可以省略圆括号

字符串

准备一个字符串:

<#assign greet = "源栈欢迎你,atai!">
统计:
  • length:返回字符串的长度
    ${greet?length} <br />
    

检索:

  • contains(substring) :判断字符中是否包含某个子串,返回布尔值
    ${greet?contains("atai")?c} <br />
  • starts_with(substring):判断字符中是否以某个子串开头,返回布尔值
    ${greet?starts_with("源栈")?c} <br />
  • ends_with(substring) :判断字符中是否以某个子串开头,返回布尔值
    ${greet?ends_with()("!")?c} <br />
  • index_of(substring,start):在字符串中从start位置开始(省略即默认为0)查找某个子串,返回找到子串的第一个字符的索引;
    ${greet?index_of("你")} <br />
    如果没有找到子串,则返回-1。
    ${greet?index_of("你", 5)} <br />

更改:

  • substring(start,end):从start位置开始截取,截取到end为止:
    ${greet?substring(0,3)} <br />
    注意和[..]的区别:
    ${greet[0..2]} <br />
  • upper_case:将字符串转为大写
    ${greet?upper_case} <br />
  • lower_case:将字符串转为小写
    ${greet?lower_case} <br />
  • html:用于将字符串中的<、>、&和“替换为对应得HTML编码
    ${greet?html} <br />
  • replace:用于将字符串中的一部分从左到右替换为另外的字符串。
    ${greet?replace("aTai", "阿泰")} <br />
  • split:使用指定的分隔符将一个字符串拆分为一组字符串
    ${greet?split(",")[0]} <br />
    ${greet?split(",")[1]} <br />
  • trim:删除字符串首尾空格
    ${greet?trim} <br />

类型转换

  • number将字符串转换为数字
    <#assign n = "23">
    ${n?number+1} <br />
  • date,time,datetime:将字符串转换为日期/时间
    <#assign now="2021-11-26 10:29:00">
    ${now?time("HH-mm")} <br />
    ${now?date} <br />
    ${now?datetime} <br />

数字、时间和布尔值

取绝对值abs

<#assign score=-98.5>
${score?abs}

取整

<#assign score=98.5>
向下:${score?ceiling} <br />
向上:${score?floor} <br />
四舍五入:${score?round} <br />

取小数位数

<#assign score=98.576>
  • 用0补足
    ${score?string(".00")}
  • 不用补足
    ${score?string(".##")}

时间除了前文已述的string(),还可以截取日期(date)和时间(time):

<#assign now=.now>
${now?date} <br />
${now?time}
布尔值string(ifTrue,ifFalse)前文已述

集合

序列(sequence)集合可以:

  • size    返回sequence的大小
  • first 返回sequence的第一个值。
  • last  返回sequence的最后一个值。
  • reverse 将sequence的现有顺序反转,即倒序排序
  • sort    将sequence中的对象转化为字符串后顺序排序
<#assign sequence=[8,2,3,10,5]>
${sequence?first} <br />
${sequence?last} <br />
${sequence?size} <br />
<hr />
${sequence?reverse?first} <br />
${sequence?sort?first}
${sequence?sort?last}
哈希(hash)表如前文所述:
  • hash?keys:所有键
  • hash?values:所有值


自定义函数

声明:关键字function,方法名Add,参数a和b,

<#function Add a b>
    <#assign sum = a+b>
    <#return sum />
</#function>

参数可以使用空格分隔,也可以使用圆括号和逗号:

<#function Add(a,b)>

函数体中

  • 不要有HTML输出(有也不会输出)
  • 必须要有一个return

调用:使用圆括号而不是$

${Add(3,5)}

尽量把逻辑封装在Controller中,而是View里面

和函数相对应,宏(macro)是能输出但不能返回的代码块:

<#macro greet name shown=true>
	<h3>源栈欢迎你 ${name}</h3>
	<#if shown>	
		<#nested>
	</#if>
</#macro>

以上是宏的声明,包括

  • 关键字macro,
  • 开发人员定义的宏的名称greet
  • 两个参数:name和shown,shown还带一个“默认值”true
  • 宏的内部还有一个<#nested>的“占位符”

已声明的宏,可以按以下方式调用:

<@greet name="新同学" shown=false>
	<p>大神小班,拎包入住</p>
</@greet>

说明:

  • name没有默认值,必须赋值
  • shown有默认值,但我们用false覆盖了默认值
  • @greet标签中的内容会代替<#nested>

#常见面试题:宏和函数的区别?#


include和import

两者都可以引入其他.ftl模板页:

<#include "shared/_header.ftl">
<#import "/shared/_footer.ftl" as footer>
说明:
  • 可以是相对路径,也可以是绝对路径(以导入freemarker时设置的root context为根目录)
  • import多一个as关键字,后接开发人员自定义名称

但是,两者的区别是很明显的。

演示当被导入模板中只有HTML内容时:

  • include直接导入模板内容并予以呈现,类似于复制粘贴
  • import不会呈现任何内容

将被导入模板内容更换为宏:

<#macro content>
<#include "shared/_header.ftl">
<@content />

<#import "/shared/_footer.ftl" as footer>
<@footer.content />

注意在import时,就需要使用footer做前缀,这在freemarker中被称之为名称空间,作用是避免被引用的模板中同名变量的冲突。

演示:同时include _header.ftl 和 _footer.ftl……

layout应用

定义一个_layout.flt作为布局页,其中一个宏,宏里面有一个放置header、footer和nested

<#macro body>
	<#include "_header.ftl">
	<hr />
	<#nested> 
	<hr />
	<#include "_footer.ftl">
</#macro>

内容页首先引入_layout,然后使用宏,嵌入正文内容

<#include "/shared/_layout.ftl">
<@body>
	<h1>注册</h1>
	<form method="post">
		<input type="text">
		<button type="submit">提交</button>
	</form>
</@>

PS:嵌入(nested)的内容可能遇到的编码问题。

原因:嵌入的.ftl文件用的编码格式和HTML指示不一致

解决办法:windows - preferences - General - Text file encoding:改为UTF-8

模板间传值

实际开发中,比如_header中的导航栏,它是可以(根据用户是否登录、有无权限等)变化的,而这些信息都是内容页(通过controller/model等)才有的,所以还要在内容页和布局页之间共享数据。

在register.ftl中声明的变量

<#assign hasLogin=false>
可以在_header中发挥作用:
<#if hasLogin>
	<span>源栈欢迎你</span>
<#else>
	<a>登录</a> | <a>注册</a>
</#if>

如果不是include而是import的话,就需要通过宏的参数进行传递。

在_layout中直接使用register中声明的变量logined(因为_layout是被register直接include的),并通过宏参数传递给_header:

<#import "_header.ftl" as header>
<@header.content hasLogin=logined />
在_header中使用参数hasLogin:
<#macro content hasLogin>
<p>我是header</p>
<#if hasLogin>

@想一想@:这样做好不好?

嵌套

实际开发中,布局还可以是嵌套的。比如还可以在_header、_footer以外的正文中加一个_sidebar,怎么办?

首先要引入一个_layoutWithSidebar.ftl,

<#macro layoutWithSidebar >
	<#include "/shared/_layout.ftl">
	<@body>	
		<div style="width:60%; float:left;">
			<#nested>
		</div>
		<div style="width:40%; float:right;">
			<#include sidebarPath>
		</div>
	</@>
</#macro>
注意这个_layoutWithSidebar.ftl:
  • 首先#include了原来的共有的_layout.ftl,
  • 然后在_layou.ftl的@body中进行了左右拆分
    • 左边是正文部分,可以嵌入注册表单等
    • 右边是侧边栏,引入哪一个侧边栏?sidebarPath确定
在register中,#include_layoutWithSidebar,指定sidebarPath:
<#include "/shared/_layoutWithSidebar.ftl">
<#assign logined=false, sidebarPath="/sidebar.ftl">
<@layoutWithSidebar >
	<h1>注册</h1>
仍然建议path总是使用绝对路径,否则这里你会懵……


作业

  1. 使用freemarker完成之前JSP作业的1-3题
  2. 利用_layout重构所有页面


Spring MVC
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

后台开发


其他:WebForm和WebApi

其他ASP.NET框架,如WebForm、WebApi……

RazorPages(Core)

微软推荐的、最新的、基于Razor页面和.NET core的新一代Web项目开发技术,包括Razor Tag Helper、Model绑定和Validation、Session/Cookie、内置依赖注入等……

MVC(Framework)

过去两年间最流行的、基于.NET Framework和MVC模式的ASP.NET MVC框架,主要用于讲解安全、性能、架构和各种实战功能演示……

C#语法

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

Java语法

面向过程的变量赋值、分支循环和函数封装;面向对象的封装、继承和多态;以及更高阶的常用类库(集合/IO/多线程……)、lambda等

Java Web开发

分层架构和综合实战

J&C

Java和C#共有的语法

全部
关键字



帮助

反馈