我们在使用 Freemarker 时,通常会结合 Spring、SpringBoot 等框架使用,很少会在原生的 Servlet 中直接使用。因此,目前很多文章的内容都是讲解如何借助这些框架去使用 Freemarker,很少有文章讲解如何在原生的 Servlet 中使用 Freemarker。为了加深对 Freemarker 使用方式的理解,本文将介绍原生 Servlet 中的 Freemarker 使用,以及可能会遇到的一些坑。
1. 配置 Freemarker
在原生 Servlet 中使用 Freemarker,需要在 web.xml 中添加如下相关的配置:
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
<servlet>
<servlet-name>freemarker</servlet-name>
<servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
<init-param>
<!--
设置 ftl 文件的根路径,FreemarkerServlet 会在此根路径下去寻找对应的 ftl 文件。
如果要访问根路径下的子文件夹的 ftl 文件,那么在 url 中需要加上子文件夹名,
例如访问 /ftl/test/test.ftl 文件,则 url 为:
http://localhost:8080/contextPath/test/test.ftl (即在访问的 url 中添加了 "/test/" 子文件夹名)
-->
<param-name>TemplatePath</param-name>
<param-value>/ftl/</param-value>
</init-param>
<init-param>
<param-name>default_encoding</param-name>
<!-- template 文件的编码格式 -->
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>ResponseCharacterEncoding</param-name>
<!-- Use the output_encoding setting of FreeMarker: -->
<param-value>fromTemplate</param-value>
</init-param>
<init-param>
<param-name>output_encoding</param-name>
<!-- The encoding of the template output; Note that you must set
"ResponseCharacterEncodring" to "fromTemplate" for this to work! -->
<param-value>UTF-8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>freemarker</servlet-name>
<url-pattern>*.ftl</url-pattern>
</servlet-mapping>
上述配置信息是对 FreemarkerServlet 进行配置,而 FreemarkerServlet 是 Freemarker 实现的 Servlet,其可以帮助我们处理 ftl 文件,渲染数据。
在 FreemarkerServlet 的初始化参数中,重点是对 ftl 文件的路径以及编码格式进行设置,需要注意如下几点:
- 设置完 ftl 文件的根路径后,注意访问的 url 与 ftl 文件夹路径的关系(具体解释可见上述配置的注释)
- 编码格式需要设置成 UTF-8,否则 ftl 文件的中文会乱码
关于其它的配置信息,可以查看官方文档。
此外,需要说明的是,本文所使用的 Freemarker 版本为 2.3.31
,对应的 maven 信息如下:
1
2
3
4
5
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
2. 使用介绍
2.1 ftl 的作用域对象
在 JSP 的 EL 表达式中,其内置了 pageScope、requestScope、sessionScope、applicationScope 四种作用域对象,方便我们获取数据。而在 FreemarkerServet 中,其同样也为我们内置了如下的四种作用域对象:
- Request:对应 HttpServletRequest
- RequestParameters:对应 HttpServletRequest 中的参数
- Session:对应 HttpSession
- Application:对应 ServletContext
2.1.1 基本使用方法
在 ftl 文件中,获取作用域对象的数据,有如下两种方式:
-
不省略作用域对象名,语法:
${作用域对象.属性名}
,示例如下:1 2
${Application.attrName} ${Request.attrName}
-
省略作用域对象名,语法:
{属性名}
,示例如下:1
${attrName}
在省略作用域对象名时,Freemarker 会先在模板中查找这个 attrName 变量。如果模板中没有创建这个变量,就会在 Request 中查找该变量,然后在 Session 中查找,最后在 Application 中查找。
2.1.2 底层实现
在了解完基本用法之后,我们再来看看 Freemare 底层是如何实现这两种数据获取方式。
不省略作用域对象名
在不省略作用域对象名时,使用方式是${作用域对象.属性名}
,Freemarer 在解析这个表达式时,会进入方法 freemarker.core.Dot#_eval
去获取作用域对象以及对应的属性值。 freemarker.core.Dot#_eval
的代码如下:
1
2
3
4
5
6
7
8
9
10
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel leftModel = target.eval(env);
if (leftModel instanceof TemplateHashModel) {
return ((TemplateHashModel) leftModel).get(key);
}
if (leftModel == null && env.isClassicCompatible()) {
return null; // ${noSuchVar.foo} has just printed nothing in FM 1.
}
throw new NonHashException(target, leftModel, env);
}
假设表达式为 ${Request.attrName}
,那么在上述代码中的第 2 行 TemplateModel leftModel = target.eval(env);
将会获得 HttpServletRequest 对应的对象(HttpRequestHashModel )。
然后,在第 4 行 return ((TemplateHashModel) leftModel).get(key)
中,get 方法实际上就是调用 freemarker.ext.servlet.HttpRequestHashModel#get
方法。HttpRequestHashModel 的 get 方法代码如下:
1
2
3
public TemplateModel get(String key) throws TemplateModelException {
return wrapper.wrap(request.getAttribute(key));
}
根据上述代码,我们可以知道该方法实际上就是去 HttpServletRequest 中获取 attrName 对应的 attribute 值。
因为 TemplateHashModel 还可能是 HttpRequestParametersHashModel、HttpSessionHashModel、ServletContextHashModel,所以此处附上这些类的 get 方法实现。
-
HttpRequestParametersHashModel
对应方法
freemarker.ext.servlet.HttpRequestParametersHashModel#get
1 2 3 4
public TemplateModel get(String key) { String value = request.getParameter(key); return value == null ? null : new SimpleScalar(value); }
-
HttpSessionHashModel
对应方法
freemarker.ext.servlet.HttpSessionHashModel#get
1 2 3 4
public TemplateModel get(String key) throws TemplateModelException { checkSessionExistence(); return wrapper.wrap(session != null ? session.getAttribute(key) : null); }
-
ServletContextHashModel
对应方法
freemarker.ext.servlet.ServletContextHashModel#get
1 2 3
public TemplateModel get(String key) throws TemplateModelException { return wrapper.wrap(servletctx.getAttribute(key)); }
在了解了 Freemarker 的底层代码实现后,我们也可以确定 FreemarkerServlet 只能帮我们获取 attribute 值,不能获取类的 property 值,如:不能获取 ServletContext 中的 contextPath (contextPath 是 ServletContext 类的 property 值,而不是 attribute 值)。
省略作用域对象名
在省略作用域对象名时,使用方式是${attrName}
,而 Freemarer 在解析这个表达式时,会进入方法 freemarker.ext.servlet.AllHttpScopesHashModel#get
。而 AllHttpScopesHashModel#get
方法则会去各个作用域对象中尝试获取对应的 attribute 值,具体实现代码如下:
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
public TemplateModel get(String key) throws TemplateModelException {
// Lookup in page scope
TemplateModel model = super.get(key);
if (model != null) {
return model;
}
// Look in unlisted models (unlistedModels 用于存储 Freemarker 的内置对象,如作用域对象 Request、Session 等)
model = (TemplateModel) unlistedModels.get(key);
if (model != null) {
return model;
}
// Lookup in request scope
Object obj = request.getAttribute(key);
if (obj != null) {
return wrap(obj);
}
// Lookup in session scope
HttpSession session = request.getSession(false);
if (session != null) {
obj = session.getAttribute(key);
if (obj != null) {
return wrap(obj);
}
}
// Lookup in application scope
obj = context.getAttribute(key);
if (obj != null) {
return wrap(obj);
}
// return wrapper's null object (probably null).
return wrap(null);
}
根据上述代码,我们也清楚了在省略作用域对象名时,Freeamarker 获取 attribute 值的顺序。
2.1.3 总结
在讲解完 ftl 的作用域对象的基本用法和实现代码之后,我们再根据官方文档 中 FreemarkerServlet 的介绍进行回顾总结:
- It makes all request, request parameters, session, and servlet context attributes available to templates through
Request
,RequestParameters
,Session
, andApplication
variables.- The scope variables are also available via automatic scope discovery. That is, writing
Application.attrName
,Session.attrName
,Request.attrName
is not mandatory; it’s enough to writeattrName
, and if no such variable was created in the template, it will search the variable inRequest
, and then inSession
, and finally inApplication
.
根据文档中的第一点,也可以看出 FreemarkerServlet 只能帮我们获取 attribute 值,不能获取类的 property 值。
而文档中的第二点也解释了省略作用域对象名时,获取 attribute 值的顺序。
2.2 获取 ContextPath
2.2.1 尝试的方案及可能的错误
在开发 html 页面时,通常会使用到 css 等静态文件,而这些静态文件的路径需要获取 contextPath。所以,我们在 ftl 文件中也要想办法获取到 contextPath。
那么,如何获取呢?首先,我们看看在 JSP 中是如何获取 contextPath。
JSP 中的获取方式如下:
1
<link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/css/bootstrap/bootstrap.css"/>
如果,你借鉴上面的写法,尝试在 ftl 文件中通过 Request 对象获取 contextPath。也许,你可能会在 ftl 文件中用如下代码获取。
1
context path : ${Request.contextPath}
但是,当实际运行时,你会看到如下的错误信息:
1
2
3
FreeMarker template error:
The following has evaluated to null or missing:
==> Request.contextPath [in template "test.ftl" at line 11, column 18]
原因是, ${Request.contextPath}
这种方式是去 HttpServletRequest 中获取名为 contextPath 的 attrbiute 值,相当于是:request.getAttribute("contextPath")
,而不是 request.getContextPath()
。具体的底层实现,可见上一章节中的 Freemarker 实现代码,对应 freemarker.ext.servlet.HttpRequestHashModel#get
。
2.2.2 解决方案
当遇到上面的错误之后,你可能会在网上搜索 “freemarker 如何获取 contextPath”。但是,网上提供的解决方案大多是基于 Spring 框架去实现的,而我们目前的场景是在原生 Servlet 环境中使用 Freemarker。
那么在没有 Spring 等其它框架的帮助下,我们如何在 ftl 文件中获取 contextPath 呢?
实际上,很简单!我们可以借助 ServletContextListener,在 context 初始化完后,将 contextPath 放进 ServletContext 的 attributes 中。具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebListener
public class WebContextListener implements ServletContextListener {
public static final String CONTEXT_PATH = "contextPath";
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
// 将 contextPath 放进 ServletContext 的 attribute 中,从而使 ftl 文件可以获得 contextPath。
servletContext.setAttribute(CONTEXT_PATH, servletContext.getContextPath());
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContext servletContext = sce.getServletContext();
if (servletContext.getAttribute(CONTEXT_PATH) != null) {
servletContext.removeAttribute(CONTEXT_PATH);
}
}
}
当将 contextPath 放进 ServletContext 的 attributes 后,可以在 ftl 文件中通过如下方式获取:
1
context path : ${Application.contextPath}
至此,你可以在 Freemarker 中像 JSP 一样轻松地获取到 ContextPath。