servlet

✏️作者:银河罐头
📋系列专栏:JavaEE

🌲“种一棵树最好的时间是十年前,其次是现在”

Servlet 是什么

Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一 个 web app.

静态页面:页面内容始终是固定不变的。就是单纯的 html(eg: 搜狗主页)

动态页面:页面内容随着输入参数不同而改变。是 html + 数据(eg: B站主页)

第一个 Servlet 程序

先写个 hello world.

写个 servlet 程序,部署到 tomcat 上,通过浏览器访问,得到 hello world 字符串。

7 个步骤:

1.创建项目

2.引入依赖

3.创建目录结构

4.编写代码

5.打包程序

6.部署程序

7.验证

看起来很繁琐,实际上除了编写代码之外,剩下的步骤都是固定的。

1.创建项目

此处我们要创建一个 maven 项目。

  • maven

maven 是一个 "工程管理"工具。

1.规范目录结构

2.管理依赖(使用了啥第三方库,都给处理好)

3.构建

4.打包

5.测试

我们主要是使用管理依赖和打包功能。

maven 是 独立的程序,但是不需要单独下载安装,IDEA已经自带了。

image-20230310143114831

2.引入依赖

servlet 对应的 jar 包

image-20230310144701501

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>hello_servlet</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>
</project>

3.创建目录结构

虽然 maven 已经帮我们自动的创建了一些目录,但是还不够。此处是需要 maven 开发一个 web 程序,还需要别的目录。

1)在main 目录下(和 java, resourses 并列),创建一个 webapp 目录

2)在 webapp 目录下创建 WEB-INF 目录

3)在 WEB-INF 目录下创建一个 web.xml 文件

image-20230310150633162

4)给 web.xml 写点东西进去

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
    <display-name>Archetype Created Web Application</display-name>
</web-app>

当前写的 servlet 程序和以往写的代码相比,有一个非常大的区别,没有 main 方法。

main 方法可以视为是汽车的发动机,有发动机才能跑。

如果有辆车它没有发动机能跑吗?有办法,挂个车头让车头拽着它跑就可以了。

(eg: 地铁都是一节一节的车厢,每一节车厢没有发动机,车头有)

我们写的 servlet 程序就是 车厢,tomcat 就是 车头,把写好的 servlet 程序扔到 webapps 目录下,相当于就是把车厢挂到车头后面了。

tomcat 如何识别 webapps 目录下哪些是需要拉着跑的目录?哪些不是?

就是靠目录下有个 WEB-INF/web.xml

此时 tomcat 就把我们写的代码加载运行起来了。

idea 只是针对 Java 代码能够进行比较准确的分析和判定。

idea 里的其他代码,包括不限于 html, css, js, xml, json, sql 如果标红,是否是错的,都不好说

4.编写代码

HttpServlet
//是 servlet api 里提供的现成的类,写servlet 代码一般都是继承这个HttpServlet

重写 doGet 方法

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //super.doGet(req, resp);
        //要删掉,父类的方法只是返回了一个 错误页面
    }
}
HttpServletRequest req//http 请求
HttpServletResponse resp//http 响应

我们写的这个 doGet 方法,不需要自己手动调用,而是交给 tomcat 调用。

tomcat 收到 get 请求,就会触发 doGet 方法

tomcat 就会 构造好 2 个 参数,req 和 resp.

req: TCP socket 中读出来的字符串,按照 http 协议的格式进行解析得到的对象。

这个对象里的属性是啥?就是和 http 请求报文格式相对应的

resp: 空的对象

程序员就需要在 doGet,根据请求 req, 结合自己的业务逻辑,构造出一个 resp 对象出来。

doGet 的工作就是根据请求计算响应,其实 resp 这个参数本质上是一个"输出型参数"

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //super.doGet(req, resp);
        //这个是在服务器的控制台里打印
        System.out.println("hello world");
        //要想把 hello world 返回到客户端,需要使用下面的代码
        //getWriter()会得到一个 Writer 对象
        resp.getWriter().write("hello world");
    }
}

此处的 writer 对象是从属于 resp 的,此时进行的 write 操作是往 resp 的 body 部分进行写入,等 resp 构造好了,tomcat 会统一的转成 http 响应的格式,然后再写 socket.

流对象不一定非得是写入 网卡,也不一定是写入硬盘。也可以写入内存缓冲区里(关键是看你代码实现的细节)。

当然,这里也不排除 write 是直接通过 resp 对象往网卡里写的。

这里还需要加入"注解"。

@WebServlet("/hello")

注解是 Java 中特殊的类,Java 专门定义了 "语法糖"来实现注解。

注解的作用,针对 一个 类/方法,进行额外的"解释说明",赋予这个类额外的功能或含义。

像以前用到的 @Override 也是"注解"(jdk 自带的注解)

此处这个 @WebServlet 注解 是把当前的类 和 一个http 请求的路径关联起来。

前面提到 doGet 是 Tomcat 收到 GET 请求的时候就会调用。具体要不要调用 doGet,还得看当前 GET 请求的路径是啥。不同的路径触发不同的代码(关联到不同的类上)。

例如,我去餐馆吃饭。

我发起请求:老板,来份蛋炒饭。

http://ip:port/蛋炒饭?葱=多放

老板收到请求之后,就会按照"制作蛋炒饭"的流程来做这份饭。(根据请求计算响应)

http://ip:port/油泼面?辣椒=多放

老板收到请求之后,就会按照"制作油泼面"的流程来做这份饭。

很明显这是 2 个不同的流程,相当于是 2 个不同的类/方法

一个 servlet 程序中,可以有很多的 servlet 类。每个 servlet 类可以关联到不同的路径(对应到不同的资源),因此此处的 多个 servlet 就实现了不同的功能。

路径和 servlet 类之间是一一对应关系。

5.打包程序

把程序编译好(得到一些 .class 文件),再把这些 .class 打成压缩包。

jar 就是 .class 构成的压缩包。

但是此处要打的是 war 包。jar 只是一个普通的 java 程序,war 则是 tomcat 专属的用来描述 webapp 的程序。

一个 war 就是一个 webapp。

借助 maven 一点击即可。

image-20230314212128318

直接双击 package 或者右键运行。

image-20230314212255667

image-20230314212449530

打包完毕后,包会生成在 target 目录下。

默认情况下,maven 打的是 jar 包,此处需要打 war, 此处需要微调下pom.xml.

<packaging>war</packaging>
//这个就描述打的包是哪一种
<build>
    <finalName>hello_servlet</finalName>
</build>
//这个描述打的 war 包的名字(可以不写)

此时再次双击 package

image-20230314213028829

得到 war 包

6.部署程序

把刚才打包好的 war 拷贝到 tomcat 的 webapps 目录中即可。

无论 tomcat 是在你同一个电脑上,还是不同电脑上都是这样拷贝的。

然后启动 tomcat.

如果 tomcat 正在运行,直接拷贝过去, tomcat 也能识别。(这个识别操作可能存在 bug,在 windows上)

实际工作中,tomcat 基本是在 linux 上运行的。

image-20230314214845587

7.验证

打开浏览器,输入 url,访问写好的这个代码

image-20230314215909840

一个 tomcat 服务器上可以部署多个网站。

小结:

刚才在浏览器地址栏中输入 url 之后,浏览器就构造了一个对应的 HTTP GET请求,发给了 tomcat,tomcat就根据第一级路径,确定了具体的 webapp,根据第二级路径,确定了是调用哪个 类。再然后通过 GET/POST 方法确定调用 hello_servlet 的哪个方法(doGet, doPost…)

上述步骤,是我们使用 servlet 最朴素的步骤,当然也可以通过一些操作来简化上述过程。

比如第 1 步的创建项目,可以使用项目模板,后续就不用手动创建目录结构了。

对于 第 5 步打包和第 6 步部署程序,可以使用 IDEA 的 Tomcat 插件,把 Tomcat 集成到 IDEA 中,就省去了手动打包,手动部署的过程,只需要按一下运行就可以自动打包部署。

更方便的部署方式

像 IDEA 这样的程序虽然功能强大, 但是也无法面面俱到. 对于一些特殊场景的功能, 开发者就可以 开发一些 “插件”. 如果需要这个插件, 就单独安装. 插件就是对程序的一些特定场景, 做出一些特定的功能的扩展。

image-20230315113732414

首次使用 smart tomcat 需要配置一下(配置一次后续就不必了)

1.新增一个 运行配置

image-20230315135529553

2.点击 + 新增配置

image-20230315135615784

3.设置一下 tomcat 所在的路径

image-20230315140933439

4.运行 tomcat

image-20230315141202166

image-20230315143809523

正常情况下,点击之后,idea 就会调用 tomcat 来运行程序了。但是当前我们的程序启动失败了。

image-20230315144031239

image-20230315144100397

tomcat 占用了 8080 这个端口,把 tomcat 关了。

再次运行 idea

image-20230315144242477

现在启动成功了。

image-20230315144415696

smart tomcat 工作原理,不是自动把 war 包拷贝了,(webapps 里是不变的),是通过另一种方式启动 tomcat。

tomcat 支持启动的时候显式指定一个特定的 webapp 目录,相当于让 tomcat 加载单个 webapp 运行。

idea 直接调用 tomcat,让 tomcat 加载当前项目中的目录,这个过程没有 打 war 包,也没有拷贝,也没有解压缩的过程。

此时,程序是可以正常运行,但是像之前 webapps 下一些已有的内容(比如欢迎页面)就没有了。

这两种部署,其实就是两种 tomcat 的运行方式,对于 context path 的理解不同。

Servlet API详解

HttpServlet

核心方法

方法名称调用时机
init在 HttpServlet 实例化之后被调用一次
destroy在 HttpServlet 实例不再使用的时候调用一次
service收到 HTTP 请求的时候调用
doGet收到 GET 请求的时候调用(由 service 方法调用)
doPost收到 POST 请求的时候调用(由 service 方法调用)
doPut/doDelete/doOptions/…收到其他请求的时候调用(由 service 方法调用)
  • init:HttpServlet 实例化是在 tomcat 首次收到了和该类相关联的请求的时候(类似于之前说的懒汉模式)

image-20230316142043878

image-20230316142440323

servlet 是 服务器上运行的代码,只要服务器不重新启动,init 就不会再次执行。

  • destroy

image-20230316143020224

这里的 destroy 能不能执行到,有待商榷.

1.如果是通过 smart tomcat 的停止 按钮,这个操作本质上是通过 tomcat 的 8005 端口,主动停止,能够触发 destroy.

2.如果是直接杀进程,此时可能来不及执行 destroy 就没了.

因此不太推荐使用 destroy, 不太靠谱.

杀死进程这种关闭方式,比通过 8005 端口关闭更常见。

  • service

收到 http 请求就会触发(路径匹配的请求).

doGet 就是在 service 中调用的。

init,destroy ,service 这 3 个方法是 HttpServlet 最关键的 3 个方法。

一个 Servlet 程序 包含很多个 Servlet, 一个 Servlet 的生死不影响 整个 Servlet 程序。

代码示例:

@WebServlet("/method")
public class MethodServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doGet");
        resp.getWriter().write("doGet");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doPost");
        resp.getWriter().write("doPost");
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doPut");
        resp.getWriter().write("doPut");
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doDelete");
        resp.getWriter().write("doDelete");
    }
}

image-20230316145400530

image-20230316145410309

其他的 doPost, doPut, doDelete 这些请求怎样构造?

1.ajax

<!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>Document</title>
</head>
<body>
    <!-- 使用这个页面构造 ajax 请求 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <script>
        $.ajax({
            type: 'get',
            url: 'method',
            success: function(body,status){
                console.log(body);
            }
        })
    </script>
</body>
</html>

image-20230316154322451

image-20230316154345917

image-20230316154845844

// 相对路径
url: 'method',
// 绝对路径
url: '/hello_servlet2/method',

绝对路径是个广义的概念。

前面学到的文件系统,绝对路径是要带盘符的;

对于 http 的 url 来说,绝对路径 和 盘符无关,是个网络路径。

编写 servlet 代码的时候,每次修改代码之后记得重启服务器。

2.postman

image-20230316145709602

image-20230316145729363

HttpServletRequest

表示的是 http 请求,这个对象 是 tomcat 自动构造的。Tomcat 其实会实现监听端口,接受连接,读取请求,解析请求,构造请求对象等一系列操作。

核心方法

方法描述
String getProtocol()返回请求协议的名称和版本。
String getMethod()返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。
String getRequestURI()从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请 求的 URL 的一部分。
String getContextPath()返回指示请求上下文的请求 URI 部分。
String getQueryString()返回包含在路径后的请求 URL 中的查询字符串。
Enumeration getParameterNames()返回一个 String 对象的枚举,包含在该请求中包含的参数的名 称。
String getParameter(String name)以字符串形式返回请求参数的值,或者如果参数不存在则返回 null。
String[] getParameterValues(String name)返回一个字符串对象的数组,包含所有给定的请求参数的值,如 果参数不存在则返回 null。
Enumeration getHeaderNames()返回一个枚举,包含在该请求中包含的所有的头名。
String getHeader(String name)以字符串形式返回指定的请求头的值。
String getCharacterEncoding()返回请求主体中使用的字符编码的名称。
String getContentType()返回请求主体的 MIME 类型,如果不知道类型则返回 null。
int getContentLength()以字节为单位返回请求主体的长度,并提供输入流,或者如果长 度未知则返回 -1。
InputStream getInputStream()用于读取请求的 body 内容. 返回一个 InputStream 对象.

query string 是键值对结构,通过 getParameter 方法根据 key 获取到 value

/猪肉的手抓饼?葱=多放&香菜=不放&辣椒=微辣

代码实例:

@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这里是设置响应的 contentType,告诉浏览器响应 body 的数据格式是啥样的
        resp.setContentType("text/html");
        //搞个 stringBuilder,把这些 api 的结果拼接起来,统一写回到响应中
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(req.getProtocol());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getMethod());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getRequestURI());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getContextPath());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getQueryString());
        stringBuilder.append("<br>");
        resp.getWriter().write(stringBuilder.toString());
    }
}

image-20230316164700215

image-20230316164645278

image-20230316164749797

@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这里是设置响应的 contentType,告诉浏览器响应 body 的数据格式是啥样的
        resp.setContentType("text/html");
        //搞个 stringBuilder,把这些 api 的结果拼接起来,统一写回到响应中
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(req.getProtocol());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getMethod());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getRequestURI());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getContextPath());
        stringBuilder.append("<br>");
        stringBuilder.append(req.getQueryString());
        stringBuilder.append("<br>");
        stringBuilder.append("<br>");
        stringBuilder.append("<br>");
        stringBuilder.append("<br>");
        //获取到 header 中所有的键值对
        Enumeration<String> headerNames = req.getHeaderNames();
        while(headerNames.hasMoreElements()){
            String headerName = headerNames.nextElement();
            stringBuilder.append(headerName + ": " +req.getHeader(headerName));
            stringBuilder.append("<br>");
        }
        resp.getWriter().write(stringBuilder.toString());
    }
}

image-20230316165540720

从请求中获取参数

1.GET,query string

在前端给后端传 2 个数字,一个是 同学的 studentId, 一个是 classId

?studentId=10&classId=20

@WebServlet("/getParameter")
public class GetParameterServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //预期浏览器会发一个形如 /parameter?studentId=10&classId=20 请求
        //借助 req 里的 getParameter 方法就能拿到 query string 里的键值对内容了
        String studentId = req.getParameter("studentId");
        String classId = req.getParameter("classId");
        resp.setContentType("text/html");
        resp.getWriter().write("studentId = " + studentId +" classId = " + classId);
    }
}

image-20230316172306620

?studentId=10&classId=20

这里的 query string 键值对,会自动被 tomcat 处理成形如 Map 这样的结构,后续就可以随时通过 key 来获取 value 了(getParameter 方法)。

如果 key 在 query string 中不存在,此时返回值就是 null

2.POST, form

对于前端是 form 表单这样格式的数据,后端还是使用 getParameter 来获取。

form 表单,也是键值对,和 query string 格式一样,只是这部分内容在 body 里。

POST postParameter HTTP/1.1

[若干 header]

studentId=10&classId=20

通过 html 的 form 标签来构造上述请求。

<!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>Document</title>
</head>

<body>
    <form action="postParameter" method="post">
        <input type="text" name="studentId">
        <input type="text" name="classId">
        <input type="submit" value="提交">
    </form>

    <!-- 使用这个页面构造 ajax 请求 -->
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <script>
        
    </script>
</body>
</html>

image-20230316174727448

点击提交按钮之后,就 404 了

image-20230316174753976

用 fiddler 抓包看下

image-20230316174644340

image-20230316175424920

@WebServlet("/postParameter")
public class PostParameterServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String studentId = req.getParameter("studentId");
        String classId = req.getParameter("classId");
        resp.setContentType("text/html");
        resp.getWriter().write("studentId = " + studentId + " classId = " + classId);
    }
}

使用 getParameter, 既可以获取到 query string 中的键值对,也可以获取到 form 表单构造的 body 中的键值对。

image-20230316200115565

此处介绍的内容,就是"前后端交互"。

浏览器通过 form 表单构造了一个 http 请求,这个请求到达 tomcat, tomcat 解析请求,构造出 request 对象,再到 servlet 里面,servlet 再来解析这里面的内容,构造响应返回给浏览器。

image-20230316203331904

3.POST, json

json, 一种非常主流的数据格式,也是键值对结构。

image-20230316213629828

可以把 body 按照这个格式来组织,前端可以通过 ajax 的方式来构造出这个内容,更简单的办法是 用 postman 构造。

image-20230316215507580

点击 send 之后,出现 404,因为 postParameter2 还没有。

@WebServlet("/postParameter2")
public class PostParameter2Servlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //通过这个方法来处理 body 为 json 格式的数据
        //把 req 对象里 body 完整读取出来
        //getInputStream
        //在流对象中读多少字节,取决于 content-length
        int length = req.getContentLength();
        byte[] buffer = new byte[length];
        InputStream inputStream = req.getInputStream();
        inputStream.read(buffer);
        //把这个字节数组构造成 string,打印出来
        String body = new String(buffer,0,length,"utf8");
        System.out.println("body = " + body);
        resp.getWriter().write(body);
    }
}

image-20230317185117845

image-20230317185128523

此处打印的结果就是从 请求的 body 中读取的内容。

image-20230317185311563

fiddler 抓包结果.

image-20230317190711671

当前 通过 json 传递数据,但是服务器这边只是把整个 body 读出来,没有按照键值对的方式去处理(还不能根据 key 来获取 value).对于这种情况,可以使用 第三方库,eg: jackson.

通过 maven 来引入第三方库。

image-20230317192115660

class Student{
    public int studentId;
    public int classId;
}
public class PostParameter2Servlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //使用 jackson 使用到的核心对象
    ObjectMapper objectMapper = new ObjectMapper();
    //readValue 就是把一个 json 格式的字符串转成一个 Java 对象
    Student student = objectMapper.readValue(req.getInputStream(),Student.class);
        }
}

image-20230317193410995

readValue: 把 json 字符串转成 Java 对象;

writeValue: 把 Java 对象转成 json 字符串.

HttpServletResponse

核心方法

image-20230318204731492

  • void setCharacterEncoding(String charset)

image-20230318204902880

浏览器默认不知道你的编码方式,只能随便猜一个。

此时需要显式的告诉浏览器,响应的编码格式是哪一种。

image-20230318205251317

设置 Content-Type 和 字符集要在 getWriter()上面.

也可以把 Content-Type 和 字符集 一起设置

resp.setContentType("text/html;charset = utf8");
  • void sendRedirect(String location)
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.sendRedirect("https://www.sogou.com");
    }
}

image-20230318212102661

不用 sendRedirect 也能实现:

resp.setStatus(302);
resp.setHeader("Location","https://www.sogou.com");
  • 关于 404 页面显示
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(404);
        //但是这里不设置 body
    }
}

image-20230318213221345

这个是 body 空着,浏览器给了个 默认的 404 页面.

@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(404);
        resp.setContentType("text/html;charset = utf8");
        resp.getWriter().write("<h1>404 没找到<h1>");
    }
}

image-20230318213427630

@WebServlet("/status")
public class StatusServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //返回一个 tomcat 自带的错误页面
        resp.sendError(404);
    }
}

image-20230318213641791

image-20230318213821980

b站的 404 页面

代码示例: 服务器版表白墙

image-20230319134231203

之前写的表白墙页面,有非常严重的问题:

1.如果刷新页面或者关闭页面重开,之前输入的消息就不见了。

2.如果在一个 机器上输入了数据,第二个机器是看不到的(这些数据都是在本地浏览器中)

解决思路:
让服务器存储用户提交的数据。

当有新的浏览器打开页面的时候,从服务器获取数据。

此处服务器就可以用来进行 “存档”、“读档”。

设计程序

写 web 程序,务必要重点考虑前后端如何交互,约定好前后端交互的数据格式。

设计前后端交互接口:请求是啥样的,响应是啥样的,浏览器啥时候发这个请求,浏览器按照啥样的格式来解析。

  • 哪些环节涉及到前后端交互?

1.点击提交,浏览器把表白信息发到服务器这里。

image-20230319135617980

2.页面加载,浏览器从服务器获取到表白信息。

image-20230319135947389

实现服务器端代码

class Message {
    public String from;
    public String to;
    public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
    private List<Message> messageList = new ArrayList<>();
    private ObjectMapper objectMapper = new ObjectMapper();
    //向服务器提交数据
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //把 body 的内容读取出来了,解析成了一个 Message 对象
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        //此处通过简单粗暴的方式来保存
        messageList.add(message);
        //此处的状态码设置可以省略,不设定默认也是 200
        resp.setStatus(200);
    }

    //从服务器获取数据
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf8");
        //这个方法完成了把 Java 对象转成了 json 字符串,并写到了 resp 的 body中
        objectMapper.writeValue(resp.getWriter(),messageList);
    }
}

doPost 做的事情,就是把 message 解析的内容往 List 填。doGet , 就是把 List 的结果返回给前端即可。

针对 doGet, 就是把 messageList 转成 json 字符串,返回给浏览器。

//objectMapper.writeValue() 也可以拆成 2 步

//把 messageList 转成 json字符串
String jsonResp = objectMapper.writeValueAsString(messageList);
System.out.println("jsonResp: " + jsonResp);
//把这个 jsonResp 写回到响应 body 中
resp.getWriter().write(jsonResp);

写到这里,表白墙的后端部分就完成了

image-20230319150529662

多 POST 几次,

image-20230319150759486

//关于 jackson API 使用
class Student {
    public int classId;
    public int studentId;
}
public class TestJackson {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //readValue 是把 json 字符串转成 Java 对象
//        String s = "{\"classId\": 10, \"studentId\": 20}";
//        //readValue 第一个参数可以写 String,也可以写 inputStream
//        Student student = objectMapper.readValue(s,Student.class);
//        System.out.println(student.classId);
//        System.out.println(student.studentId);

        //writeValue , writeValueAsString 是把 一个 Java 对象转成 json 字符串
        Student student = new Student();
        student.classId = 10;
        student.studentId = 20;
        String s = objectMapper.writeValueAsString(student);
        System.out.println(s);
    }
}

实现前端代码

  • POST

POST 是点击提交按钮的时候发起的。GET 是页面加载的时候发起的。

要实现前后端交互,要使用 ajax, 就要引入 jquery

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>

image-20230319155103304

//4.[新增] 给服务器发起 post 请求,把上述数据 提交到服务器
let body = {
    "from": from,   
    "to": to,
    "message": msg
}
$ajax({
    type:'post',
    url:'message',
    data: strBody,
    contentType:"application/json;charset=utf8",
    success: function(body) {
        console.log("数据发布成功");
    }
});

当前 body 是个 JS 对象,不是字符串。网络传输只能传字符串,不能传对象。

image-20230319155500708

JS 内置了 json 转换的库。

点击提交按钮之后:

image-20230319183841594

接下来实现"读档操作"。

  • GET

让浏览器通过 ajax 发送 GET 请求。

image-20230319185451141

服务器本身是存储多条消息的,此时就需要从服务器上把所有消息都获取到。

//[新增] 在页面加载的时候,发送 GET 请求,浏览器从服务器获取数据,添加到页面中
        $.ajax({
            type: 'get',
            url: 'message',
            success: function(body){
                //此处拿到的 body 就是一个 js 的对象数组了
                //本来服务器返回的是一个 json 字符串, 而 jquery 的 ajax 能够识别并自动把 json 字符串转成 js 对象数组
                //遍历这个数组, 把元素取出来, 构造到页面中
                let containerDiv = document.querySelector('.container');
                for(let message of body){
                    //针对每个元素构造一个 div
                    let rowDiv = document.createElement('div');
                    rowDiv.className = 'row message';
                    rowDiv.innerHTML = message.from + ' 对 ' + message.to + ' 说: ' + message.message;
                    containerDiv.appendChild(rowDiv);
                }
            }
        });

重新打开或者加载浏览器页面,触发 GET 请求

image-20230319191754654

此时数据是保存在服务器内存中的,重启服务器,数据仍然丢失。

要想持久化保存,就需要写入文件中(硬盘)

1.直接使用流对象写入文本文件

2.借助数据库

数据存入数据库

  • 创建数据表

此处只有一个表。

message(from, to, message)

image-20230319211759342

mysql>  create table message(`from` varchar(20),`to` varchar(20),message varchar(1024));

from, to 都是 sql 里的关键字,可以用``引起来。

//DBUtil.java
//通过这个类,把数据库连接过程封装一下。
    //此处把 DBUtil 当成工具类,提供 static 方法让其他代码调用
public class DBUtil {
    //静态成员是跟随类对象的,类对象在整个进程中只有唯一一份
    //静态成员相当于也是唯一的实例(单例模式,饿汉模式)
    private static DataSource dataSource = new MysqlDataSource();
    static{
        //使用 静态代码块,针对 dataSource 进行初始化
        ((MysqlDataSource)dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false");
        ((MysqlDataSource)dataSource).setUser("root");
        ((MysqlDataSource)dataSource).setPassword("123456");


    }
    //通过这个方法来建立连接
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
    //通过这个方法来断开连接,释放资源
    public static void close(Connection connection, PreparedStatement statement,ResultSet resultSet) {
        //此处3个 try catch 分开写更好,避免前面的异常导致后面的 代码不能执行
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
//MessageServlet.java
class Message {
    public String from;
    public String to;
    public String message;
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
//    private List<Message> messageList = new ArrayList<>();
    private ObjectMapper objectMapper = new ObjectMapper();
    //向服务器提交数据
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //把 body 的内容读取出来了,解析成了一个 Message 对象
        Message message = objectMapper.readValue(req.getInputStream(),Message.class);
        //此处通过简单粗暴的方式来保存
//        messageList.add(message);
        save(message);
        //此处的状态码设置可以省略,不设定默认也是 200
        resp.setStatus(200);
    }

    //从服务器获取数据
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf8");
        //这个方法完成了把 Java 对象转成了 json 字符串,并写到了 resp 的 body中
        //objectMapper.writeValue(resp.getWriter(),messageList);

        List<Message> messageList = load();
        //把 messageList 转成 json字符串
        String jsonResp = objectMapper.writeValueAsString(messageList);
        System.out.println("jsonResp: " + jsonResp);
        //把这个 jsonResp 写回到响应 body 中
        resp.getWriter().write(jsonResp);
    }

    //提供一对方法
    //往数据库中存一条消息
    private void save(Message message){
        //JDBC 操作
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            //1.建立连接
            connection = DBUtil.getConnection();
            //2.构造一个 sql 语句
            String sql = "insert into message values(?, ?, ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1, message.from);
            statement.setString(2, message.to);
            statement.setString(3, message.message);
            //3.执行 sql
            statement.executeUpdate();


        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            //4.关闭连接
            DBUtil.close(connection,statement,null);
        }
    }
    //从数据库取所有消息
    private List<Message> load(){
        List<Message> messageList = new ArrayList<>();
        Connection connection = null;
        ResultSet resultSet = null;
        PreparedStatement statement = null;
        try{
            //1.和数据库建立连接
            connection = DBUtil.getConnection();
            //2.构造 sql
            String sql = "select * from message";
            statement = connection.prepareStatement(sql);
            //3.执行 sql
            resultSet = statement.executeQuery();
            //4.遍历结果集合
            while(resultSet.next()){
                Message message = new Message();
                message.from = resultSet.getString("from");
                message.to = resultSet.getString("to");
                message.message = resultSet.getString("message");
                messageList.add(message);
            }

        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            //5.需要释放资源,关闭连接
            DBUtil.close(connection,statement,resultSet);
        }
        return messageList;
    }
}

image-20230320141544377

把数据保存在数据库之后,即使重启服务器,打开浏览器页面,数据仍然存在。

但是这种是需要手动刷新页面才会显示新的消息,有没有办法能够自动更新消息?可以使用 websocket(应用层协议),实现服务器主动推送数据。

Cookie 和 Session

cookie 存储在浏览器(客户端)所在主机的硬盘上,浏览器会根据域名来存储。

cookie 的用途是很多的,cookie 有个最典型的应用,标识用户的身份信息。

比如网站有个登录功能。

image-20230320144212506

所谓 过期,可能是客户端或服务器把 cookie 删了。

image-20230320145144558

代码示例: 实现用户登陆

1.编写登录页面

image-20230320150816420

image-20230320150829772

<!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>
    <form action="login" method="post">
        <input type="text" name="username">
        <br>
        <input type="password" name="password">
        <br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

image-20230320155053175

2.编写 LoginServlet 处理登录请求

image-20230320160016820

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
    }
}
		if(!username.equals("zhangsan") && !username.equals("lisi")){
            //登陆失败
            //重定向到登录页面
            System.out.println("登陆失败,用户名错误!");
            resp.sendRedirect("login.html");
            return;
        }
        if(!password.equals("123")){
            //登陆失败
            System.out.println("登陆失败,密码错误!");
            resp.sendRedirect("login.html");
            return;
        }
        //登陆成功
        //1.创建一个会话
        HttpSession session = req.getSession(true);
		//2.把当前的用户名保存到会话中
        //3.重定向到主页

所谓的会话是一个键值对,key 是 sessionId, value 是 HttpSession 对象。每个客户端登录的时候都会有一个这样的键值对(会话),服务器要管理多个这样的会话,服务器可以搞一个哈希表,把这些会话组织起来。

image-20230320163143951

注意:这里的 HttpSession 对象也是一个键值对,

setAttribute, getAttribute 来存取键值对,这里的键值对是自定义数据。

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        //验证用户名密码是否正确,
        //正常情况下用户名密码会用数据库来保存,此处直接写死
        //此处约定 用户名合法的是 zhangsan,lisi.密码合法的是 123
        if(!username.equals("zhangsan") && !username.equals("lisi")){
            //登陆失败
            //重定向到登录页面
            System.out.println("登陆失败,用户名错误!");
            resp.sendRedirect("login.html");
            return;
        }
        if(!password.equals("123")){
            //登陆失败
            System.out.println("登陆失败,密码错误!");
            resp.sendRedirect("login.html");
            return;
        }
        //登陆成功
        //1.创建一个会话
        HttpSession session = req.getSession(true);
        //2.把当前的用户名保存到会话中,此处的 HttpSession 又可以当成是一个 map 使用
        session.setAttribute("username",username);
        //3.重定向到主页
        resp.sendRedirect("index");
    }
}

会话这里,服务器是如何组织的?

image-20230320164502484

getSession(true): 存在就返回现成的,不存在就创建;

getSession(fasle): 存在就返回现成的,不存在返回 null (不创建);

参数的 true 和 false 表示是否要创建新的。

浏览器的无痕模式(如果你想上一些网站,查询学习资料,又不想被别人发现,就可以用这模式,不会留下登录状态和历史记录)

无痕模式,本质上有自己的一套 cookie,和其他页面的 cookie 不混淆(浏览器的特殊功能的特殊处理)

3.编写 IndexServlet 生成主页

@WebServlet("/index")
public class IndexServlet extends HttpServlet {
    //通过 重定向,浏览器发起的是一个 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //先判定用户的登录状态
        //如果用户还没登录,要求先登录
        //如果用户已经登陆了,则要求用户的用户名显示到页面上
        HttpSession session = req.getSession(false);
        if(session == null){
            //未登录状态
            System.out.println("用户未登录");
            resp.sendRedirect("login.html");
            return;
        }
        //已经登陆
        String username = (String) session.getAttribute("username");
        //构造页面
        resp.setContentType("text/html;charset=utf8");
        resp.getWriter().write("欢迎 " + username + " 回来!");
    }
}

image-20230320195902827

启动服务器,浏览器登录。

image-20230320194354173

image-20230320194452676

交互过程:

image-20230320195954551

只要完成登陆之后,后面请求多次服务器,也都会带上刚才 cookie 的值(sessionId).

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值