引入问题
在 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
2. 提出问题
那么,既然 TemplateEngine 这个引擎对象,只需要一个实例, 要不要直接创建单例模式呢?
答:这里不能创建成单例模式,所谓单例模式,指的是整个进程里面,只有一个实例。那么,整个 Tomcat 服务器 实际上就是一个进程。
所以,此处的 TemplateEngine 并不是进程级别的单例,而是 webapp 级别的单例。
3. ServletContext 对象的重要方法
二、代码示例:多个 Servlet 共享数据
1. WriterServlet 类
// 使用这个 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); } }
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 对应字符串的值为空,这很好理解,我们并没有先存值,取出来的当然为空了。
展示结果2
如果我们先往 ServletContext 对象中存入值【 hello world 】,之后,取 message 值的时候,就可以拿到【 hello world 】了。很显然,这也是一种键值对的结构。
展示结果3
如果我们重启服务器,如果再次直接从 reader 路径发送 HTTP 请求的时候,那么对应的 message 依然为空。这是为什么呢?
因为,ServletContext 是一个 Servlet 程序中全局的储存信息的空间,服务器开始就存在,服务器关闭就销毁。当重启服务器,自然是会清空之前的 ServletContext 中的内容。
然而,我们当前使用的是 IDEA 内置的 Smart Tomcat 插件,我们通过这个插件来开启服务器,它并没有保存到本地 Tomcat 目录下的 webapps 目录,所以重启服务器,就会将之前的内容清空。
但是,如果我们将程序打包成 jar 包,在本地的目录进行部署,再通过 【 startup.bat 】这种手动方式开启 Tomcat 服务器,可能又是不同的情况。
分析代码
三、提出问题
基于上面的 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 类的步骤。
六、结合 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>
展示结果
总结 ServletContext 和 Listener
ServletContext 和 Listener 是 Servlet 提供的两个机制,在 web 开发中,它们常搭配 Thymeleaf 一起使用。
我们可以做一个比喻:
ServletContext 相当于一个冰箱,既可以往里面放东西,也可以从里面取东西,这些 “东西”,可以是普通的变量,也可以是对象。
Listener 监听器相当于一个摄像头,当有人往冰箱中放数据 / 取数据 的时候,它就能够监测到。在我们当前举的例子中,监听器是用来监测 TemplateEngine 类 所创建出来的实例,然而,在实际开发中,它也能够监听一些其他的数据。总之,监听器也是一个较为重要的机制,不仅局限于当前的逻辑。
Thymeleaf 新的总结流程
- 构造一个 html 模板.
这里涉及到一些 Thymeleaf 对应的一些特殊属性,例如:【th:text, th:if, th:each, th:href …】
- 初始化模板引擎
(1) 创建 TemplateEngine 实例
(2) 创建 ServletContextTemplateResolver 实例,并规定好目录的路径,前后缀…
(3) 把 engine 和 resolver 关联起来
(4) 把 engine 对象存放到 ServletContext 里面
- 在业务代码中,使用引擎渲染
(1) 从 ServletContext 中获取到 engine
(2) 构建好 WebContext
(3) 最终,通过 process 方法进行模板渲染(变量替换)