详解模板引擎二

引入问题

在 Thymeleaf 的使用流程中,每一次我们需要进行模板渲染的时候,就需要初始化一个 TemplateEngine 实例,同时也需要创建一个 ServletContextTemplateResolver 实例…

然而,在实际开发中,可能一个 Servlet 类就对应着一个 HTTP 响应,那么,难道每次创建一个类,就需要实例化初始化一个 TemplateEngine 实例吗?

答:其实并不需要。因为一个完整的项目中,只需要创建一个 TemplateEngine 实例即可,即只用初始化一次即可。而为了完成这样的目的,就需要使用 Servlet 中的 ServletContext 和 “监听器”。

一、什么是 ServletContext

ServletContext 是一个 Servlet 程序中全局的储存信息的空间,服务器开始就存在,服务器关闭就销毁。

  • Tomcat 在启动时,它会为每个 webapp 都创建一个对应的 ServletContext
  • 一个 WEB 应用中的所有的 Servlet 共享同一个 ServletContext 对象
  • 可以通过 HttpServlet.getServletContext() 或
    HttpServletRequest.getServletContext() 获取到当前 webapp 的 ServletContext 对象.
  • Context 英文原义为 “环境” / “上下文”。此处的 ServletContext 对象就相当于一个 webapp 的 “上下文”。这就和语文一样,结合上下文,就能够分析出语境。

1. 理解 ServletContext

3

2. 提出问题

那么,既然 TemplateEngine 这个引擎对象,只需要一个实例, 要不要直接创建单例模式呢?

答:这里不能创建成单例模式,所谓单例模式,指的是整个进程里面,只有一个实例。那么,整个 Tomcat 服务器 实际上就是一个进程。
所以,此处的 TemplateEngine 并不是进程级别的单例,而是 webapp 级别的单例。

3. ServletContext 对象的重要方法

方法描述
void setAttribute(String name, Object obj)设置属性(键值对)
Object getAttribute(String name)根据属性名获取属性值, 如果 name 不存在, 返回 null
void removeAttribute(String name)删除对应的属性

二、代码示例:多个 Servlet 共享数据

1. WriterServlet 类

// 负责往 ServletContext 对象中写数据
// 浏览器通过一个形如 /writer?message=abc 访问 WriterServlet,
// 在 message=abc 这个键值对存到 ServletContext 对象中
@WebServlet("/writer")
public class WriterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=UTF-8");

        // 1. 先从请求中获取到 message 参数
        String message = req.getParameter("message");
        // 2. 取出 ServletContext 对象 ( 这个对象是 Tomcat 在加载 webapp 的时候自动创建的)
        ServletContext context = this.getServletContext();
        // 3. 往对象写入键值对
        context.setAttribute("message", message);
        // 4. 返回响应
        resp.getWriter().write("<h3> 存储 message 成功 </h3>");
    }
}

2. ReaderServlet 类

// 使用这个 Servlet 从 ServletContext 对象中读取数据
// 就把刚才的 WriterServlet 存储的数据取出来
@WebServlet("/reader")
public class ReaderServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=UTF-8");

        // 1. 获取到一个 ServletContext 对象
        ServletContext context = this.getServletContext();
        // 2. 从 Context 中获取到刚刚存储的值
        String message = (String)context.getAttribute("message");
        // 3. 把取的数据显示在页面上
        resp.getWriter().write("message: " + message);
    }
}

展示结果

展示结果1

如果我们直接从 reader 路径发送 HTTP请求,可以看到,message 对应字符串的值为空,这很好理解,我们并没有先存值,取出来的当然为空了。

1

展示结果2

如果我们先往 ServletContext 对象中存入值【 hello world 】,之后,取 message 值的时候,就可以拿到【 hello world 】了。很显然,这也是一种键值对的结构。

2

展示结果3

如果我们重启服务器,如果再次直接从 reader 路径发送 HTTP 请求的时候,那么对应的 message 依然为空。这是为什么呢?

3

因为,ServletContext 是一个 Servlet 程序中全局的储存信息的空间,服务器开始就存在,服务器关闭就销毁。当重启服务器,自然是会清空之前的 ServletContext 中的内容。

然而,我们当前使用的是 IDEA 内置的 Smart Tomcat 插件,我们通过这个插件来开启服务器,它并没有保存到本地 Tomcat 目录下的 webapps 目录,所以重启服务器,就会将之前的内容清空。

但是,如果我们将程序打包成 jar 包,在本地的目录进行部署,再通过 【 startup.bat 】这种手动方式开启 Tomcat 服务器,可能又是不同的情况。

分析代码

1

2

三、提出问题

基于上面的 ServletContext 机制,我们知道了,它就像一个冰箱,可以往里面放东西,也可以从里面取东西。所以,如果我们像之前说的,一个 webapp 目录下,所有的 Servlet 共用一个 TemplateEngine 对象,这实现起来就不麻烦了。我们只需要把TemplateEngine 初始化好,同时放到 ServletContext 对象里,后面的其他 Servlet 就不必再初始化了,直接取出刚才的 engine 对象即可。

然而,上面的代码让我们发现,取数据的时候并不是第一时间就能够取的,( 需要先往里面存入数据,等存好了数据,又要约定一些代码来告诉调用者,已经存好了数据。)所以,这样未免也太过于麻烦,总是需要时间差。

那么,有什么办法可以实现其他的 Servlet 都能够第一时间地就获取到一个初始化好的TemplateEngine 实例呢?

答:Servlet 为我们提供了 “监听器” 机制,可以解决上面的问题。

请继续往下看。

四、什么是监听器

在 Servlet 运行过程中,会有一些特殊的 “时机”,可以供我们来执行一些我们自定义的逻辑,监听器的作用就是让开发人员可以在这些 特殊时机 “插入代码”。

Servlet 中的监听器种类有很多,例如:

  • 监听 HttpSession 的创建 / 销毁,属性变化
  • 监听 HttpServletRequest 的创建 / 销毁,属性变化
  • 监听 ServletContext 的创建 / 销毁,属性变化

使用监听器之前,我们需要自定义一个类实现接口 ServletContextListener,并重写 contextInitialized 和 contextDestroyed 这两个方法

  • contextInitialized 这个方法表示:ServletContext 初始化完后,会调用这个方法;
  • contextDestroyed 这个方法表示:ServletContext 销毁之前,会调用这个方法。

五、代码示例:创建一个监听器

// 光创建这个类还不够,还需要让 Tomcat 能够识别这个类,通过 @WebListener 注解来进行描述
@WebListener
public class MyListener implements ServletContextListener {

    /**
     * ServletContext 初始化完后,会调用这个方法
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("ServletContext 初始化!");
        // 获取 ServletContext 对象,通过方法的参数获取
        ServletContext context = servletContextEvent.getServletContext();
        context.setAttribute("message", "hello world");
    }

    /**
     * ServletContext 销毁之前,会调用这个方法
     * 显然,当前的逻辑,我们并不会用到下面的方法
     */
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

由于当前监听器的机制,能够让 ServletContext 初始化完后,直接调用contextInitialized 方法,所以,我们在这个方法中,就可以往 ServletContext 对象中存数据。关于代码的细节,我在上面的代码中,已经给出注解。

展示结果

当我们再次从 reader 路径发送 HTTP 请求的时候,那么 message 对应的值就是我们在监听器中直接设置的值了。这样一来,就少了另外创建 WriterServlet 类的步骤。

0

六、结合 ServletContext 和 Listener

定好思路

  • 创建一个 ThymeleafConfig 类,在这个类中,我们实现一个监听器,此外,在代码中,创建一个 TemplateEngine 实例,并往 ServletContext 中存放。这样一来,在同一个 webapp 目录下,所有 Servlet 程序都能够第一时间内,从 ServletContext 取出 engine 实例。

  • 对当前博客,之前写的 【 th: each 】案例进行修改,去除掉 init 初始化方法。

1. ThymeleafConfig 类 (关键类)

@WebListener
public class ThymeleafConfig implements ServletContextListener {

    /**
     * 初始化 TemplateEngine
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext context = servletContextEvent.getServletContext();

        // 1. 创建 engine 实例
        TemplateEngine engine = new TemplateEngine();
        // 2. 创建 resolver 实例
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(context);
        resolver.setPrefix("WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("UTF-8");
        engine.setTemplateResolver(resolver);
        // 3. 把创建好的 engine 对象存放到 ServletContext 中
        context.setAttribute("engine", engine);
        System.out.println("TemplateEngine 初始化完毕!");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

2. Servlet 代码

class Person2 {
    public String name;
    public String phone;

    public Person2(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }
}

@WebServlet("/each2")
public class EachServlet2 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset = UTF-8");
        List<Person2> persons = new ArrayList<>();
        persons.add(new Person2("Jack", "123"));
        persons.add(new Person2("Rose", "456"));
        persons.add(new Person2("Ron", "789"));
        persons.add(new Person2("Bruce", "321"));
        persons.add(new Person2("Lisa", "654"));

        WebContext webContext = new WebContext(req, resp, this.getServletContext());
        webContext.setVariable("persons", persons);

        // 从 ServletContext 对象中取出 engine 实例
        ServletContext context = this.getServletContext();
        TemplateEngine engine = (TemplateEngine)context.getAttribute("engine");

		// 下面的 each 表示 html 模板文件
        engine.process("each", webContext, resp.getWriter());
    }
}

3. html 模板文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电话本</title>
</head>
<body>
    <ul>
        <li th:each="person : ${persons}">
            <span th:text="${person.name}"></span>
            <span th:text="${person.phone}"></span>
        </li>
    </ul>
</body>
</html>

展示结果

2

总结 ServletContext 和 Listener

ServletContext 和 Listener 是 Servlet 提供的两个机制,在 web 开发中,它们常搭配 Thymeleaf 一起使用。

我们可以做一个比喻:

ServletContext 相当于一个冰箱,既可以往里面放东西,也可以从里面取东西,这些 “东西”,可以是普通的变量,也可以是对象。
Listener 监听器相当于一个摄像头,当有人往冰箱中放数据 / 取数据 的时候,它就能够监测到。在我们当前举的例子中,监听器是用来监测 TemplateEngine 类 所创建出来的实例,然而,在实际开发中,它也能够监听一些其他的数据。总之,监听器也是一个较为重要的机制,不仅局限于当前的逻辑。

Thymeleaf 新的总结流程

  1. 构造一个 html 模板.
    这里涉及到一些 Thymeleaf 对应的一些特殊属性,例如:【th:text, th:if, th:each, th:href …】

  2. 初始化模板引擎
    (1) 创建 TemplateEngine 实例
    (2) 创建 ServletContextTemplateResolver 实例,并规定好目录的路径,前后缀…
    (3) 把 engine 和 resolver 关联起来
    (4) 把 engine 对象存放到 ServletContext 里面

  3. 在业务代码中,使用引擎渲染
    (1) 从 ServletContext 中获取到 engine
    (2) 构建好 WebContext
    (3) 最终,通过 process 方法进行模板渲染(变量替换)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值