Servlet知识详解(2)

一、Cookie和Session

1. Cookie和Session的介绍

Cookie 是HTTP 协议中一个重要的字段,同时也是浏览器和客户端这边保存数据的一种比较古老的方式。

Cookie从哪来?服务器产生的,通过HTTP 响应 的set-Cookie 字段来进行设置,返回给浏览器的。
Cookie怎么存?在浏览器这边存储,浏览器会根据域名/地址 ,来分别存储Cookie。
Cookie到哪去?会在下次请求中自动被添加到请求中,发给服务器。
Cookie存的是什么?存的是字符串,是键值对结构的字符串,此处的键值对都是程序员自定义的。

Session 称为“会话”,HTTP协议,本身是无状态的
无状态的意思是:某个用户A,对服务器进行了两次请求,第一次请求和第二次请求,对于服务器来说,是感知不到关联关系的。但实际开发中,又需要有状态,用户登录完成之后,后续再访问这个网站的时候,就需要知道用户的身份信息。

Session是在服务器中存储的,可以简单地把Session想象成一个hash表,key 就是 sessionId,value 就是程序员自定义的数据(可以存储用户的身份信息),下面会实现一个Session,自己去体会。

Cookie 和 Session 之间需要相互配合。

2. 浏览器和服务器使用Cookie和Session的交互逻辑

在这里插入图片描述
这张图的关键点就在于:服务器可以得知第二次这个请求是哪个用户发来的,并根据用户信息去更新数据

详细过程
如:首先用户登录后,因为服务器中没有存储对应的sessionId,因此就为其分配sessionId,value中就保存它的用户名和密码等信息,响应中有Set-Cookie字段来告诉浏览器,该用户的session为什么什么。等该用户点到购物车的时候,浏览器就拿着Cookie(包含sessionId)给服务器,服务器就会根据该sessionId去查找,找到对应的sessionId后,取出value里面的userId,再根据userId去数据库中查找,找出指定userId的购物车数据。

上面只不过是典型的登录流程,除了服务器在Set-Cookie中放入sessionId外,其实可以不放sessionId,而是可以直接把用户信息序列化,通过Set-Cookie返回给浏览器,这样也是可行的

只需要把浏览器的两个请求能够联系起来就行,进行联系的不一定非得是sessionId,也可以是userId,也可以是其它ID,都是可以的。
使用sessionId只是常用的做法,这样做的好处是,可以方便实现“注销操作“。在服务器这边把 session 哈希表中对应的键值对给删了即可。返回sessionId 也有助于保护用户的隐私。

此处所说的cookie和session 的联动,主要是针对主流的网页进行实现。如果是更加新的一些网页,实现的时候完全可以不依赖 cookie,cookie 存在的目的就是为了在浏览器这边本地存储。还可以使用 localStorage 和 indexDB 来代替cookie 。

3. 对Cookie和Session操作的API

上面讲的都是一些理论知识,不过有了前面的理论,了解Cookie和Session 的交互过程,我们才能更好的在代码中体现。

Servlet中对上面的流程大部分都已经封装好了,我们实际上操作的过程不算很多,只需要一些简单的API调用即可。

核心方法:
HttpServletRequest 类中的相关方法(常用)

方法描述
HttpSession getSession()在服务器中获取会话. 参数如果为 true, 则当不存在会话时新建会话; 参数如果为 false, 则当不存在会话时返回 null
Cookie[] getCookies()返回一个数组, 包含客户端发送该请求的所有的 Cookie 对象. 会自动把Cookie 中的格式解析成键值对

HttpServletResponse 类中的相关方法

方法描述
void addCookie(Cookie cookie)把指定的 cookie 添加到响应中

HttpSession 类中的相关方法(常用)
一个 HttpSession 对象里面包含多个键值对. 我们可以往 HttpSession 中存储任何我们需要的信息。

方法描述
Object getAttribute(String name)该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null
void setAttribute(String name, Object value)该方法使用指定的名称绑定一个对象到该 session 会话
boolean isNew()判定当前是否是新创建出的会话

本来sessionId和value就是一个键值对,而其中的value 就是HttpSession ,HttpSession里存储的又是键值对,该类里面的key和value都是由程序员自己定义的,之所以用键值对的方式表示HttpSession,是为了方便程序员自定义数据

4. 实现简单的用户登录

前端:

    <form action="login" method="POST">
        用户名:<input type="text" name="username">
        <br>
        密码:<input type="text" name="password">
        <br>
        <input type="submit" value="登录">
    </form>

页面:在这里插入图片描述
后端:
实现LoginServlet 类

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 1. 先从请求的 body 中读取 用户名 和 密码
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        // 2. 判定一下用户名密码是否正确(此处就不读数据库了, 直接固定用户名密码, 就是 zhangsan 123
        if (!"zhangsan".equals(username) || !"123".equals(password)) {
            // 登录失败!!
            resp.getWriter().write("登录失败!");
            return;
        }
        System.out.println("登录成功");
        // 3. 登录成功, 则创建出一个会话来~, 会话不存在就创建
        //    会话是根据请求中的 sessionId 来查的. sessionId 是 在 Cookie 中的.
        //    但是此处是首次登录, 此时请求中是没有 Cookie (Cookie 是服务器返回)
        //    此处就会触发 "找不到就创建" 这样的流程~
        //    同时这里进行的操作: 先创建出一个 HttpSession 对象(作为 value)
        //    再生成一个随机的字符串, 作为 sessionId (作为 key)
        //    把这个 key 和 value 插入到 hash 表中~~
        //    同时把这个生成的 sessionId 通过 Set-Cookie 字段返回给浏览器
        HttpSession httpSession = req.getSession(true);
        //    还可以存入程序猿自定义的数据, 可以存入身份信息(用户名和登录次数)
        httpSession.setAttribute("username", "zhangsan");
        httpSession.setAttribute("loginCount", 0);
        //登录成功之后,就创建了HttpSession对象,同时生成了sessionId
        // 4. 让页面跳转到主页, 使用重定向的方式实现即可
        resp.sendRedirect("index");
    }
}

实现IndexServlet 类:

@WebServlet("/index")
public class IndexServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 根据当前用户请求中的 sessionId, 获取到用户信息, 并显示到页面上.
        resp.setContentType("text/html; charset=utf-8");
        // 1. 判定当前用户是否已经登录了. (请求中有没有 sessionId, 以及 sessionId 是否合法)
        //    如果会话不存在, 就不能创建了~ 只是查询, 不是登录
        HttpSession httpSession = req.getSession(false);
        if (httpSession == null) {
            // 当前没有找到合法会话, 当前用户尚未登录, 重定向到 login.html, 让用户进行登录
            resp.sendRedirect("login.html");
            return;
        }
        // 2. 如果用户已经登录, 就可以从 HttpSession 中拿到用户信息了.
        String username = (String) httpSession.getAttribute("username");
        Integer loginCount = (Integer) httpSession.getAttribute("loginCount");
        loginCount = loginCount + 1;
        httpSession.setAttribute("loginCount", loginCount);
        // 3. 返回一个 HTML 页面
        StringBuilder html = new StringBuilder();
        html.append("<div>用户: " + username + "</div>");
        html.append("<div>访问次数: " + loginCount + "</div>");
        resp.getWriter().write(html.toString());
    }
}

当第一次登录的时候,返回页面的是(输入的用户名和密码必须正确):
在这里插入图片描述
上面代码需要注意的点:
第一次客户端给服务器的第一次交互,是一个登录请求:
请求:
在这里插入图片描述
响应:
在这里插入图片描述
还有一个重要的header字段:
在这里插入图片描述
正是这个重定向,触发了浏览器和服务器之间的第二次交互。

第二次交互:是一个访问主页的请求。
请求:Cookie里面的value和第一次交互的Set-Cookie的value 是同一个值。
在这里插入图片描述
响应:
首行和body:
在这里插入图片描述

二、上传文件(Part类)

上传文件也是日常开发中的一类常见需求. 在 Servlet 中也进行了支持。

核心方法:
HttpServletRequest 类方法

方法描述
Part getPart(String name)获取请求中给定 name 的文件
Collection< Part> getParts()获取所有的文件

Part 类方法

方法描述
String getSubmittedFileName()获取提交的文件名
String getContentType()获取提交的文件类型
long getSize()获取文件的大小
void write(String path)把提交的文件数据写入磁盘文件

前端部分:简单地设置一个选择文件,并提交。(写一个upload代码,包含form表单,可以让用户选择文件)
:action中的upload 是与Java代码中的@WebServlet注释里面的是相同的(去掉 / ),method 是按了提交按钮的方法,enctype是上传文件需要设置的一个重要的属性,因为此处是以文件的形式上传,则选择multipart/form-data 。input标签里的type 属性为file ,则是去找地址去选择文件;name属性与Java代码中的getPart方法 里的参数名是一致的

    <form action="upload" method="POST" enctype="multipart/form-data">
        <input type="file" name="MyImage">
        <input type="submit" value="提交图片">
    </form>

在这里插入图片描述

后端代码:(创建一个UploadServlet,来处理刚才页面提交的form表单)
注:使用到Servlet来处理上传文件的时候,需要在开头加上@MultipartConfig这个注释,加上这个注释,Servlet才能正确地读取请求中的文件内容。Part 是处理和获取文件信息的关键类,里面的许多方法能够处理和获取文件的信息。

@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Part part = req.getPart("MyImage");//参数与html中input的file类型的name相匹配
        System.out.println(part.getSubmittedFileName());//获取真实文件名
        System.out.println(part.getContentType());//获取文件格式类型
        System.out.println(part.getSize());//获取文件大小
        part.write("c:/tmp/image.jpg");//将传入的文件放到什么地方,并且重命名为image.jpg
        resp.getWriter().write("upload ok");//返回的响应
    }
}

服务器处理的响应结果:
在这里插入图片描述

如果没有@MultipartConfig 注释,此时提交文件,就会出现一个异常:
在这里插入图片描述
正常情况下,我们去抓包,去查看POST包中的body,是图片的内容,可以看到是转化为二进制,我们是看不懂的。
在这里插入图片描述
下载文件,只需要把文件从磁盘上读取出来,放到body中即可,Content-Type 也根据文件的类型来进行设置,如是图片类型,就设置为jpg格式;如果是audio类型,就设置为mp3格式。

三、模板引擎

Servlet 只是前后端交互的API ,Servlet并不关心,服务器返回的页面是什么。例如:我们在浏览器中去查询不同的查询词,页面返回的结果也各不相同,就说明这个页面是动态的。

因此很多时候,需要给前端返回一个html 页面,同时还希望,html 中的有些内容是计算得到的(这个HTML 并不是一个静态的页面,而是动态可变的)

使用模板引擎,就能够完成这个动态的工作。如果没有模板引擎,要想返回一个html ,通常会使用字符串拼接的方式来进行。

1. 体会用字符串拼接的方式

从客户端中访问服务器的页面时,query string里面要带有name=? 的键值对。

@WebServlet("/html")
public class HtmlServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 请求传过来一个 query string, 这里有一个参数, 叫做name.
        // 返回的页面里, 就包含这个 name 的值
        // 用户的请求中的 name 不同, 返回的页面就不同
        String name = req.getParameter("name");
        resp.setContentType("text/html; charset=utf-8");
        StringBuilder html = new StringBuilder();
        html.append("<html>");
        html.append("<head><title>标题</title></head>");
        html.append("<body>");
        html.append("<h3>" + name + "</h3>");
        html.append("</body>");
        html.append("</html>");
        resp.getWriter().write(html.toString());
    }
}

如果在访问服务器的地址为:127.0.0.1:8080/20220413/html?name=lisi,那么服务器返回的页面为:
在这里插入图片描述
去查看源代码,就会发现是一个更接近于标准格式的html 了。
在这里插入图片描述

2. 用Thymeleaf来实现简单的页面

前端:

<!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>thymeleaf 示例</title>
</head>
<body>
    <!-- 我们期望 h3 里的内容是可变的, 是 Servlet 代码里计算出来之后传过来的 -->
    <h3 th:text="${message}"></h3>
</body>
</html>

后端:

@WebServlet("/helloThymeleaf")
public class HelloThymeleafServlet extends HttpServlet {
    private TemplateEngine engine = new TemplateEngine();

    @Override
    public void init() throws ServletException {
        // 完成 Thymeleaf 的初始化操作

        // 创建一个 模板解析器 对象
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(this.getServletContext());
        // 让模板解析器, 来加载模板文件. 这里的前缀就表示模板文件所在的目录. 正因为如此, 模板文件必须要放到 WEB-INF 目录中
        // 这里设置的前缀和后缀, 就是告诉模板引擎, 要加载哪些文件到内存中, 以备后用.
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        // 把解析器对象, 给设置到 engine 对象中.
        engine.setTemplateResolver(resolver);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 0. 在执行模板渲染(把刚才写的模板html代码里面的 message 变量进行替换)之前, 要先进行初始化~
        //    初始化操作只需要执行一次即可. 放到 init 方法中来实现
        // 1. 先从参数中读取出用户要传过来的 message 的值. (从 query string 中读取)
        String message = req.getParameter("message");
        // 2. 把当前从请求中读取出来的 message 的值和 模板 中的 ${message} 关联起来.
        WebContext webContext = new WebContext(req, resp, this.getServletContext());
        webContext.setVariable("message", message);
        // 3. 进行最终的渲染.
        String html = engine.process("hello", webContext);
        System.out.println(html);
        resp.getWriter().write(html);
    }
}

只要在访问服务器的时候带上query string,query string为message=什么,然后getParameter来获取message后面的值。之后就能返回给前端message值(webContext.setVariable("message", message);),String html = engine.process("hello", webContext);进行渲染。因此页面就会根据访问服务器时候,带上的query string中的变量message,来根据message的值来返回不同的页面。

最后调用process才是正在地去执行变量的替换,里面的第一个参数为html页面的前缀,不包含后缀,而第二个变量是webContext,还可以有第三个变量,如果是不想打印最后的html,就可以传第三个变量为resp.getWriter().write()

在这里插入图片描述
在这里插入图片描述
在服务器的控制台中打印的html,与原来的html进行比较:
在这里插入图片描述

3. 实现web版的猜数字

1.约定好前后端交互的方式
GET(方法) /guessNum(路径) 。
通过这个请求,来获取到一个页面(页面里面包含了数字的基本的界面,同时在服务器这边初始化(让服务器生成一个1-100 之间的要去猜的数字))。
响应内容就是一个页面。

POST(方法) /guessNum(路径)
处理一次猜的过程,页面上有一个 输入框,还有一个提交按钮,点击提交按钮,就会把输入的数据交给服务器。服务器收到这个数据之后,判断一下当前这个数字是不是猜对了,返回一个页面,返回的页面就包含了 猜大/猜小/猜对 。

2.创建一个 GuessNumServlet 类。
a) 先实现一个 doGet方法,来处理第一个交互接口。
b) 再实现一个 doPost方法,来处理第二个交互接口。

上面用字符串拼接的方式,是非常麻烦的,如果标签一多,就会很乱。当出错的时候,也很难去查错。因此使用字符串拼接的方式对开发是非常不好的。

此时就引入一个 模板引擎——Thymeleaf,这个是Spring里面规定要使用的模板引擎,虽然他不算最好的,甚至还有些麻烦,但是为了先学习它,到后面学习Spring 的时候就会简单很多了。

模板引擎,可以让我们单独写一个html 文件,固定的地方可以做到不变,而不固定的地方使用一些特殊符号去占位,再在程序运行中,把占位符给替换掉即可

a) 首先,先在中央仓库中去导入 Thymeleaf 这个包,可以选择一个不那么新的版本,将jar包复制到pom.xml 中。
b) 先编写一个模板文件(就是一个HTML ,只不过把一些可变的数据给单独拿出来了),格式如:th:text=“${变量}” ,th 就是 thymeleaf 的缩写,意思是这个属性是 thmeleaf 所提供的,text 表示里面的类型是个字符串。括号里面是一个具体的变量,就可以在 Java 代码中,把这个变量的值给填写进来
c) 在webapp 的WEB-INF 的文件夹中,再创建一个template 文件夹,在文件夹里面再去创建下面的这个html 。
在这里插入图片描述
前端代码:

<body>
    <form action="guessNum" method="POST">
        <input type="text" name="num">
        <input type="submit" value="猜数字">
    </form>

    <!-- 这个 th:if 是一个条件显示的逻辑. if 后面的表达式为 真 就显示里面的标签. 如果表达式为 假 就不显示 -->
    <div th:if="${!newGame}">
        <!-- 这两个代码, 得是猜了之后才会显示的, 开始一局新游戏的时候不显示. -->
        <div th:text="${result}"></div>
        <div th:text="${count}"></div>
    </div>
</body>

后端代码:
a) 编写Servlet 代码,创建一个 HelloThymeleafServlet ,在doGet方法中,就把刚才这个模板给返回出来
b) TemplateEngine 是Thymeleaf 中的最核心的类,Template是模板,Engine是引擎。
c) 创建一个模板解析器对象,搭配 ServletContext 来使用。
在这里插入图片描述
Context翻译成“上下文”,如Context Path 。每一个webapp 里面都有一个Context一个webapp 里面可以有多个Servlet,这些Servlet 共用同一个Context 对象。这个Context 对象是Tomcat 加载 webapps 的时候所创建的对象。Tomcat 在webapps 目录中有多个webapp,每个webapp 都会创建出一个对应的 Context 对象。ServletContext存在的意义是 让同一个 webapp 的多个 Servlet之间能够共享数据

在一个webapp 里面任意Servlet 类中,都可以通过getServletContext 获取到这个 上下文对象。

@WebServlet("/guessNum")
public class GuessNumServlet extends HttpServlet {
    private TemplateEngine engine = new TemplateEngine();

    // toGuess 表示要猜的数字
    private int toGuess = 0;
    // count 表示要猜的次数
    private int count = 0;

    @Override
    public void init() throws ServletException {
        // 对模板引擎进行初始化
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(this.getServletContext());
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        engine.setTemplateResolver(resolver);
    }

    // 获取到页面的初始情况, 并且初始化, 生成一个待猜的数字
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 1. 先生成一个待猜的数字
        Random random = new Random();
        // [0, 100) + 1 => [1, 100]
        toGuess = random.nextInt(100) + 1;
        count = 0;
        // 2. 返回一个页面.
        //    这里是开启一局新的游戏
        WebContext webContext = new WebContext(req, resp, getServletContext());
        webContext.setVariable("newGame", true);
        engine.process("guessNum", webContext, resp.getWriter());
    }

    // 处理一次猜的过程
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 1. 从请求中, 读取出用户提交的数字的内容
        String parameter = req.getParameter("num");
        int num = Integer.parseInt(parameter);
        String result = "";
        // 2. 和 toGuess 进行比较
        if (num < toGuess) {
            result = "猜低了";
        } else if (num > toGuess) {
            result = "猜高了";
        } else {
            result = "猜对了";
        }
        // 3. 自增猜的次数
        count++;
        // 4. 构造一个结果页面, 能够显示当前猜的结果.
        WebContext webContext = new WebContext(req, resp, getServletContext());
        webContext.setVariable("newGame", false);
        webContext.setVariable("result", result);
        webContext.setVariable("count", count);
        engine.process("guessNum", webContext, resp.getWriter());
    }
}

有个细节是,猜数字提示 猜大猜小猜对 和 猜的次数 不是一开始就显示的,而是猜了第一次之后才显示。因为在前端中设置了 th:if=${"newGame"},是Thymeleaf中的判断语法。而在Java代码中,因为访问服务器是GET 方法,因此在doGet方法中将该newGame属性设置为true,而用户输入猜的数字给服务器去处理,是POST 方法,因此在doPost方法中将newGame属性置为false,才能进入到显示猜的对错和次数

4. 实现模板引擎具体代码小结

Thymeleaf 里面的重要类/概念:
1.TemplateEngine 模板引擎的本体,功能就是渲染模板。在这里插入图片描述
2.WebContext 就是一个键值对结构,(Thymeleaf 是用一个专门的类来表示,很多其他的模板引擎就是直接拿一个 hash表 来表示)。模板中的 ${message} 和 Java 中的变量关联起来。

3.ServletContextTemplateResolver 模拟解析器。功能就是加载模板文件,然后告诉TemplateEngine 模板文件是哪些。
在这里插入图片描述
大致流程:换汤不换药
在这里插入图片描述
模板引擎最大的优点,就是让Java 代码和 界面的HTML 代码分离开了

5. Thymeleaf模板语法和核心类

常用语法:

命令功能
th:text在标签体中展示表达式求值结果的文本内容
th:[HTML标签属性]设置任意的 HTML 标签属性的值
th:if当表达式的结果为真时则显示内容,否则不显示
th:each循环访问元素

th:text 和 th:if 我们都已经使用过,而 th:[HTML标签属性] 的用法:如果是img 标签,里面有个src 属性,如果我们想通过Thymeleaf Java代码来设置src的值,就可以在模板中写出:th:src=${url} ,类似的也是如此,常用的像a 标签里面的href属性、input 标签的value 属性等。

th:each 是循环渲染,有的时候,在服务器这边提供的数据可能是一组(以数组/List等方式来呈现),在页面显示的时候,也就需要能够把这一组中每个元素都进行渲染,最终循环的方式展示出来。下面就直接举个例子。

在这里插入图片描述

6. 基于 th:each 实现显示电话号码

前端:
persons拿到的就是等下服务器传过来的数组,person就是persons里面其中一个元素。person.name 和 person.phone 都会随着 person 的不同而改变。

<body>
    <ul>
        <li th:each="person : ${persons}">
            <span th:text="${person.name}"></span>
            <span th:text="${person.phone}"></span>
        </li>
    </ul>
</body>

后端:

class Person {
    public String name;
    public String phone;

    public Person(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }
}
@WebServlet("/each")
public class EachServlet extends HttpServlet {
    private TemplateEngine engine = new TemplateEngine();

    @Override
    public void init() throws ServletException {
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(this.getServletContext());
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        engine.setTemplateResolver(resolver);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("朱俊锐","110"));
        persons.add(new Person("张三","112"));
        persons.add(new Person("李四","985"));
        WebContext webContext = new WebContext(req,resp,this.getServletContext());
        webContext.setVariable("persons",persons);
        engine.process("thymeleafEach",webContext, resp.getWriter());
    }
}

7. 模板引擎固定套路

在这里插入图片描述

8. ServletContext

我们前面已经了解到,模板引擎中创建 ServletContextTemplateResolver 类 和 WebContext 类里面的参数都包含有ServletContext,但是它不需要实例化,因为在Tomcat中已经帮我们实例化了,我们直接使用this.getServletContext 就能够拿到该实例了。

Tomcat与webapp、ServletContext、Servlet之间的关系:
在这里插入图片描述
每个webapp就是同一个Context Path 下的webapp,这里的webapp与文件名为webapp的不是同一个东西。在同一个Context Path中,可以创建很多个Servlet,这么多个Servlet 却是可以使用同一个Servlet Context 的。

但是正常来说,加载不同模板的HTML ,一般会使用不同的ServletContext,因为它就是一个键值对结构。因此在模板中,参数不相同的情况下,使用同一个ServletContext是没有问题的,但是在工作开发时最好不要

Tomcat 给每个 webapp 自动创建出的ServletContext对象,每个webapp 有一个实例,存在的意义是让同一个webapp 的多个Servlet 之间能够共享数据。下面就会用到它的共享性。

上面说过,TemplateEngine 在同一个webapp中只有一个,但是又不能用单例模式去实例化,因为如果是使用单例模式,那么就是整个Tomcat进程只有一个TemplateEngine的实例了,但是我们要的是一个webapp一个TemplateEngine实例,因此ServletContext能够实现让 TemplateEngine在一个webapp中只有一个实例

webapp就是 tomcat的webapps 目录下的子目录。
在这里插入图片描述

8.1 了解ServletContext的使用

它存在的意思是:能够让同一个wabapp中的多个Servlet 之间能够共享数据。因此ServletContext 就提供了一组 get/set 接口。

ServletContext 对象的重要方法:

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

可以看到 ServletContext 和 HttpSession 类中的方法很类似, 也是在内部组织了若干个键值对结构, 相当于一个哈希表。

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

目的:了解Servlet中共享数据的使用
创建WriterServlet类:
在客户端访问服务器的时候为:127.0.0.1:8080/20220413/writer?message=hello

@WebServlet("/writer")
public class WriterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String message = req.getParameter("message");//先获取到query string
        ServletContext servletContext = this.getServletContext();//直接用this来获取ServletContext对象
        servletContext.setAttribute("message",message);//设置键值对
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("<h3> 存储 message 成功 </h3>");
    }
}

创建ReaderServlet类:

@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");
        ServletContext servletContext = this.getServletContext();
        String message = (String) servletContext.getAttribute("message");//获取在WriterServlet中存储的值
        resp.getWriter().write("message: "+message);
    }
}

先访问路径为writer的Servlet,有:127.0.0.1:8080/20220413/writer?message=hello,此时页面上显示:
在这里插入图片描述
此时再去访问路径为reader的Servlet,有:127.0.0.1:8080/20220413/reader,此时在页面上显示:
在这里插入图片描述
代码细节:
在这里插入图片描述

8.3 什么是监听器 (Listener)

因为我们需要使用ServletContext 来存储一个TemplateEngine 的实例,因此就需要提前把TemplateEngine 实例化好,同时放到 ServletContext 的对象里,后面的 Servlet 就不必再对它初始化了,直接取出存好的engine 即可。

为了保证 其它的Servlet 都能够第一时间地获取到一个初始化好的TemplateEngine实例,决定就可以在 ServletContext 被创建好之后,就第一时间的给 TemplateEngine 进行实例化/初始化/保存。

但是刚才讲到,ServletContext 是Tomcat自己创建的,如何让 ServletContext创建好之后,第一时间就执行自己自定义的代码呢?Servlet就提供了一个机制,Listener(监听器)接口,因此就可以使用Listener 来监听ServletContext 的初始化完毕操作。在JS 中的onclick 也是监听器的效果。

如:固定套路:
步骤
1.创建一个新的类实现ServletContextListener 接口,重写其中的 contextinitialized 方法,这个方法就是初始化完毕之后被自动调用的。
2.给这个新的类,加上@WebListener 的注释,加上这个注释之后才能够被 Tomcat 识别出来并进行加载。
3.在方法里面去获取 ServletContext 是通过 方法中的参数ServletContextEven 来获取到的。

@WebListener
public class ThymeleafConfig implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        TemplateEngine engine = new TemplateEngine();
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(servletContext);
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        engine.setTemplateResolver(resolver);
        servletContext.setAttribute("engine",engine);
        System.out.println("engine 初始化完毕");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

上面的这个代码不一定仅仅是用来初始化 TemplateEngine 的,也可能是用来初始化其它的内容。

9. 实现在线相册

前端:
原本的代码:这个代码放在webapp文件下。CSS也一样

<head>
    <meta charset="UTF-8">
    <title>相册</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- 第一组图片 -->
<figure class="sample">
    <img src="image/1.jpg" alt="sample1" />
    <figcaption>
        <div>
            <h2>Deconovo</h2>
            <h4>Classic Sculptures</h4>
        </div>
    </figcaption>
    <a href="image/1.jpg"></a>
</figure>
<figure class="sample">
    <img src="image/2.jpg" alt="sample2" />
    <figcaption>
        <div>
            <h2>Deconovo</h2>
            <h4>Classic Sculptures</h4>
        </div>
    </figcaption>
    <a href="image/2.jpg"></a>
</figure>
<figure class="sample">
    <img src="image/3.jpg" alt="sample3" />
    <figcaption>
        <div>
            <h2>Deconovo</h2>
            <h4>Classic Sculptures</h4>
        </div>
    </figcaption>
    <a href="image/3.jpg"></a>
</figure>
</body>

但是我们使用模板引擎的方式,会节省很多空间:
关键要点:
1.使用th:each 分别构造出多个figure 标签。
2.期望 Java 代码中传过来一个 images 标签,数组的每个元素是一个 image 对象,这里面有两个属性,url和name
3.把image的url和name放到html 的合适位置上。
注:这个代码因为是引擎代码,放在template文件目录下

<body>
    <!-- 通过 th:each 循环生成多个 figure 标签, 每个这个标签就对应一张图 -->
    <figure class="sample" th:each="image : ${images}">
        <img th:src="${image.url}" alt="sample1" />
        <figcaption>
            <div>
                <h2 th:text="${image.name}"></h2>
            </div>
        </figcaption>
        <a th:href="${image.url}"></a>
    </figure>
</body>

此处不引入CSS了,主要是体会模板引擎的作用。

为了引入图片,在webapp的目录底下,创建一个image文件夹,再在里面放入三个jpg 图片。

后端代码:
同样地,先创建好ThymeleafConfig 类来初始化TemplateEngine :

@WebListener
public class ThymeleafConfig implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        TemplateEngine engine = new TemplateEngine();
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver(servletContext);
        resolver.setPrefix("/WEB-INF/template/");
        resolver.setSuffix(".html");
        resolver.setCharacterEncoding("utf-8");
        engine.setTemplateResolver(resolver);
        servletContext.setAttribute("engine",engine);
        System.out.println("engine 初始化完毕");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

创建ImageServlet类:
在这个类里面就要生成一个image 对象的数组,传给网页模板,并返回。这个Image对象的数组要给定一个指定的路径去查找,就从图片所在的目录去查找,看都有哪些图片,根据图片的信息构造出image数组。对于查找文件的绝对路径,可以使用ServletContext的getRealPath方法来获取

class Image {
    public String name;
    public String url;
}

@WebServlet("/image")
public class ImageServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html; charset=utf-8");
        // 1. 扫描指定的路径 /webapp/image 路径, 看有多少个图片, 构造成 List<Image>
        List<Image> images = loadImage();
        // 2. 把 images 给构造到模板页面中.
        ServletContext context = this.getServletContext();
        TemplateEngine engine = (TemplateEngine) context.getAttribute("engine");
        WebContext webContext = new WebContext(req, resp, context);
        webContext.setVariable("images", images);
        String html = engine.process("images", webContext);
        resp.getWriter().write(html);
    }

    private List<Image> loadImage() {
        // 扫描 /webapp/image 目录
        List<Image> images = new ArrayList<>();
        // 需要获取到 /webapp/image 这个目录在磁盘上的绝对路径
        ServletContext context = this.getServletContext();
        String path = context.getRealPath("/image");
        System.out.println(path);
        // 根据这个路径来看里面有哪些图片文件了.
        File imageRoot = new File(path);
        File[] files = imageRoot.listFiles();
        for (File f : files) {
            Image image = new Image();
            image.name = f.getName();
            image.url = "image/" + f.getName();
            images.add(image);
        }
        return images;
    }
}

此时我们直接访问服务器,服务器就会显示出图片了,并且效果跟没用模板引擎的代码是一样的。

效果图:
在这里插入图片描述
此时我们还可以设置提交图片的形式,再在上面的效果图中展示出来。
因此前端部分要提供一个提交文件的页面来实现:
注:提交文件不要忘了enctype属性

<body>
    <form action="upload" method="POST" enctype="multipart/form-data">
        <input type="file" name="MyImage">
        <input type="submit" value="上传文件">
    </form>
</body>

创建UploadServlet类:

// 这个注解在上传文件的功能中是必要的
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String path = getServletContext().getRealPath("/image");
        Part part = req.getPart("MyImage");
        part.write(path + "/" + part.getSubmittedFileName());
        resp.sendRedirect("image");//为了能够显示页面
    }
}

目录展示:
在这里插入图片描述

  • 48
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 34
    评论
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zjruiiiiii

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

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

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

打赏作者

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

抵扣说明:

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

余额充值