Servlet3 (1) 基础入门

347 阅读12分钟

1. WEB通信模型

概念: web通信就是客户端和服务端基于HTTP协议下的请求和响应的过程:

  • 协议:web服务器必须遵守 HTTP 协议,其底层是 TCP/IP 协议:
    • TCP 负责将数据完整的送到目的地,尽管路途中可能会将数据拆成若干小块。
    • IP 负责把数据准确地送到目的地。
  • 客户端 client:指人类用户或者浏览器。
  • 服务端 server:指物理主机硬件或WEB服务器应用软件。

z-image/WEB通信模型.jpg

1.1 GET请求

概念: GET请求无请求体,用于从服务器快速获取资源:

  • GET请求模板:通信协议://服务器IP或名称:服务器端口/资源所在目录/资源?查询串
    • 所请求的资源,可以是HTML,PDF,Servlet等。
    • 查询串记录着请求携带的数据,长度有限制。
  • GET请求理解:
    • 客户端:"嘿!服务器,给我拿(get)下某服务器中的某资源,哦对了,这有一些给你的查询串参数,请快点!"
    • 服务端:"好的,我会去拿(get)那个资源,也谢谢你提供的查询串参数,不过有一点要说明,咱俩的HTTP协议中,可没有包含 '快一点' 这一项!"
  • GET请求观察:
    • 开发首页 index.html,布局 "HelloWorld" 文字,部署,运行。
    • 打开浏览器开发者模式,切换到 Network 选项卡。
    • 发送get请求 {{tomcat}}/servlet3/index.html
    • 点击 Network 中出现的本次请求路径,查看请求响应信息。
    • 查看 Headers 选项卡中的 General 请求常规信息:
      • RequestURL:请求的URL,包括查询串。
      • RequestMethod:请求的方式。
      • Status Code:请求状态码和状态码文字信息。
    • 查看 Headers 选项卡中的 Response Header 响应头信息:
      • Content-Length:响应数据的字节大小。
      • Content-Type:响应的数据MIME类型。
      • Date:响应发生的时间。
    • 查看 Headers 选项卡中的 Request Header 请求头信息:
      • Accept:设定请求可接收的MIME数据类型列表,响应数据类型不匹配会拒绝。
      • User-Agent:请求的浏览器信息。
    • 查看 Headers 选项卡中的 Query String Prameters 查询串信息。
    • 查看 Response 选项卡中的响应的数据。

布局: index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="#" method="post">
    <label for="username-ipt">账号</label>
    <input type="text" id="username-ipt" name="username">
    <label for="password-ipt">密码</label>
    <input type="password" id="password-ipt" name="password">
    <button type="submit">登陆</button>
</form>

</body>
</html>

2.2 POST请求

概念: POST就是使用请求体携带额外数据的GET请求,牺牲了速度,但安全,传输量更大:

  • POST请求理解:
    • 客户端:"嘿!服务器,请把这个请求提交(post)给某服务器中的某资源,别忘了看我的请求体,那里面有我发送的一些重要的表单数据!"
    • 服务端:"好的,我会去寻找那个地址上的资源,等我找到了,我会把你在请求体中发送的数据交给它!"
  • POST请求观察:
    • index.html 中开发表单,action="#",method="post",部署运行。
    • 访问首页 {{tomcat}}/servlet3/index.html
    • 打开浏览器开发者模式,切换到 Network 选项卡。
    • 提交表单,点击 Network 中出现的本次请求路径,查看请求响应信息。
    • 额外查看 Headers 选项卡中的 Form Data 请求体表单数据。

布局: index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="#" method="post">
    <label for="username-ipt">账号</label>
    <input type="text" id="username-ipt" name="username">
    <label for="password-ipt">密码</label>
    <input type="password" id="password-ipt" name="password">
    <button type="submit">登陆</button>
</form>

</body>
</html>

2.3 其他请求

概念: 大部分浏览器仅支持前两种类型的HTTP请求:

  • get:请求URL上的一个资源。
  • post:请求URL上的一个资源,且要求服务器接收附加到请求体中的信息。
  • head:请求URL上的一个资源,但要求只返回响应头信息,不返回响应体。
  • trace:请求URL上的一个资源,要求回送请求消息,以便客户端测试或者排错。
  • put:请求URL上的一个资源,且携带额外一些包含的信息。
  • delete:请求删除URL上的一个资源。

2. Servlet入门

概念: web容器擅长提供静态页面,不擅长操作 just-in-time 即时动态页面,若想得到一个即时显示当前的系统时间的HTML,则web容器必须求助一些辅助程序,如servlet:

  • 客户端发送请求,请求URL指向web容器中的辅助程序servlet。
  • web容器分析出这个请求是发给servlet的,而非直接获取静态资源。
  • web容器启动并运行客户端指定的servlet,将请求和请求参数一并转交。
  • servlet执行 service(),构造一个插入了当前系统时间的HTML静态代码。
  • servlet将HTML静态代码返回给web容器。
  • web容器关闭servlet,并将HTML静态代码响应给客户端。
  • 客户端解析HTML静态代码并渲染到浏览器中,展示最终页面。

servlet3依赖:javax.servlet-api

配置: pom.xml

 <!-- 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>

2.1 XML配通

概念: 采取继承,重写,配置的纯手动模式来配置一个servlet:

  • 开发 XmlServlet 类继承 HttpServlet
    • 实现 Servlet 接口和继承 GenericServlet 接口均已过时。
  • 重写 HttpServlet 中的 doGet()doPost()
    • 方法参数:(HttpServletRequest req, HttpServletResponse resp)
  • 配置servlet:在 web.xml 文件中添加:
    • 父标签:<servlet>:配置servlet基本信息,包括名称和类全名。
    • 父标签:<servlet-mapping>:配置servlet映射信息,包括名称和路由。
    • 子标签:<servlet-name>:配置servlet名称,不对外暴露。
    • 子标签:<servlet-class>:配置servlet类全名。
    • 子标签:<url-pattern>:配置servlet路由,须以 / 开头。
  • 开发 doGet():设置响应MIME类型和编码,获取响应流并回写数据:
    • resp.setContentType("text/html;charset=utf-8")
    • resp.getWriter().print("数据")
    • resp.getOutputStream().print("数据")
  • PM:{{tomcat}}/servlet3/api/servlet/xml

源码: XmlServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

public class XmlServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // Request Headers [Accept:text/html...]
        resp.setContentType("text/html;charset=utf-8");

        // response data
        PrintWriter writer = resp.getWriter();
        writer.println("<!DOCTYPE html>");
        writer.println("<html>");
        writer.println("<head>");
        writer.println("<meta charset=\"UTF-8\">");
        writer.println("<title>xml-servlet</title>");
        writer.println("</head>");
        writer.println("<body>");
        writer.println("系统时间:" + new Date());
        writer.println("</body>");
        writer.println("</html>");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

配置: web.xml

    <servlet>
        <servlet-name>xmlServlet</servlet-name>
        <servlet-class>com.yap.start.servlet.XmlServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>xmlServlet</servlet-name>
        <url-pattern>/api/servlet/xml</url-pattern>
    </servlet-mapping>

2.2 注解配通

概念: servlet3+版本支持使用 @WebServlet 注解省略servlet手动配置:

  • 开发 AnnotationServlet 类,完成继承重写步骤。
  • 对servlet类添加 @WebServlet 注解:
    • 必须使用 valueurlPatterns 来指定一个或多个路由。
  • 开发 doGet():回写插入了当前系统时间的页面。
  • PM:{{tomcat}}/servlet3/api/servlet/annotation_a
  • PM:{{tomcat}}/servlet3/api/servlet/annotation_b

源码: AnnotationServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

@WebServlet({"/api/servlet/annotation_a", "/api/servlet/annotation_b"})
public class AnnotationServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        // Request Headers [Accept:text/html...]
        resp.setContentType("text/html;charset=utf-8");

        // response data
        PrintWriter writer = resp.getWriter();
        writer.println("<!DOCTYPE html>");
        writer.println("<html>");
        writer.println("<head>");
        writer.println("<meta charset=\"UTF-8\">");
        writer.println("<title>xml-servlet</title>");
        writer.println("</head>");
        writer.println("<body>");
        writer.println("系统时间:" + new Date());
        writer.println("</body>");
        writer.println("</html>");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

2.3 初始化参数

概念: servlet在初始化时可以设置一些独享的String类型参数:

  • XML配通可以在 <servlet> 中使用 <init-param> 完成初始化参数设置:
    • <param-name>tel</param-name><param-value>182</param-value>
  • 注解配通可以在 @WebServlet 中使用 initParams 属性完成初始化参数设置:
    • initParams = { @WebInitParam(name="tel", value="182")... }
  • doGet() 中获取当前servlet的初始化参数(无法设置):
    • super.getServletConfig().getInitParameter(KEY)

源码: ServletConfigXmlServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class ServletConfigXmlServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(super.getServletConfig().getInitParameter("tel"));
        System.out.println(super.getServletConfig().getInitParameter("email"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

配置: web.xml

    <servlet>
        <servlet-name>initParamXmlServlet</servlet-name>
        <servlet-class>com.joezhou.start.servlet.ServletConfigXmlServlet</servlet-class>
        <init-param>
            <param-name>tel</param-name>
            <param-value>18210210122</param-value>
        </init-param>
        <init-param>
            <param-name>email</param-name>
            <param-value>yy06200210@163.com</param-value>
        </init-param>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>initParamXmlServlet</servlet-name>
        <url-pattern>/api/servlet/servlet_config_xml</url-pattern>
    </servlet-mapping>

源码: ServletConfigAnnotationServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(value = "/api/servlet/servlet_config_annotation", initParams = {
        @WebInitParam(name = "tel", value = "1297849057"),
        @WebInitParam(name = "email", value = "1297849057@qq.com"),
})
public class ServletConfigAnnotationServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(super.getServletConfig().getInitParameter("tel"));
        System.out.println(super.getServletConfig().getInitParameter("email"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

2.4 全局参数

概念: web容器在启动时可以设置一些所有servlet共享的String类型参数:

  • web.xml 中添加 <context-param> 完成全局参数设置:
    • <param-name>loc</param-name><param-value>china</param-value>
  • doGet() 中获取全局参数(无法设置):
    • super.getServletContext().getInitParameter(KEY)

源码: ServletContextServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/api/servlet/servlet_context")
public class ServletContextServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(super.getServletContext().getInitParameter("loc"));
        System.out.println(super.getServletContext().getInitParameter("type"));
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

配置: web.xml

    <context-param>
        <param-name>loc</param-name>
        <param-value>china</param-value>
    </context-param>
    <context-param>
        <param-name>type</param-name>
        <param-value>human</param-value>
    </context-param>

3. Servlet生命周期

概念: servlet是单例多线程的,web容器会每个指向servlet的请求都分配一个单独的线程:

  • 启动服务器时:
    • web容器寻找部署文件(DD文件)web.xml
    • web器找所有的servlet类,如 LifeCycleServlet.class 并加载它。
    • web容器调用这个类的构造方法来实例化,所以我们自己不要重写它的构造器。
    • web容器为每一个servlet建立一个ServletConfig对象,负责读servlet的初始化参数,还能来访问ServletContext(负责读取web信息,如工程首页等)。
  • 请求到来时:
    • web容器分析出这个请求指向的是一个servlet 。
    • web容器创建 HttpServletRequest 请求对象和 HttpServletResponse 响应对象。
    • web容器根据请求的URL找到 LifeCycleServlet
    • web容器调用servlet类的 init() 初始化这个类,只执行一次。
    • web容器为该请求分配线程,用于调用 service() 并支付 reqresp
    • service() 根据请求头决定调用 doGet()doPost(),并支付 reqresp
    • doGet()doPost() 开始执行代码并做出响应。
    • 线程结束,线程销毁或被遣返回线程池,reqresp 离开作用域,等待GC。
    • 客户得到最终响应。
  • 关闭服务器时:
    • web容器调用servlet的 destroy() 做一些清理工作,只执行一次。

若servlet配置了 <load-on-startup> 且值大于等于0,则 init() 在servlet的实例化时执行,值越小加载优先级越高,若值小于0或不配置,则 init() 在第一次servlet请求时执行。

源码: LifeCycleServlet.java

package com.yap.start.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(value = "/api/servlet/life_cycle", loadOnStartup = 1)
public class LifeCycleServlet extends HttpServlet {
    @Override
    public void destroy() {
        System.out.println("destroy()...");
    }

    @Override
    public void init() {
        System.out.println("init()...");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("doGet()...");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req, resp);
    }
}

Servlet生命周期小说

第一章:启动服务器

我叫 HelloServlet,我是一个字节码文件,我出生在一个混沌的世界中,我对出生的印象很模糊,不知道自己的父母是谁。

我听见有人在敲我的门,打开门后看见的是一个一脸严肃的人。

"你好,我叫类加载器,特意来要带你离开这里。"

"带我去哪里?"

"Tomcat服务器已经启动了,我需要把一些类加载到内存中去..."

"所有的类都要加载吗?"

"不,只需要一些特殊的类,它们都是 HttpServlet 的孩子..."

"你是怎么找到我的?"

"Web容器手里有一份部署文件名单(web.xml),我通过上面的地址()找到了你,你是最后一个。"

“我一直想问,我是从哪来的?是你口中那个Web容器创造的我吗?”

"没错,服务器启动的时候,Web容器就已经根据 HelloServlet.java 来编译创造了你,它是你的本体。"

"那我的本体现在在哪?"

"因为它比较重要,所以它不会被发布到服务器,仍在硬盘世界里,你是找不到它的,其余更多的内容你就不要再问了。"

我哦了一声,跟着它终于来到了服务器中,映入眼帘的是一个公告板,上面写着一句话:“项目的首页是index.html”。

“这是什么意思?”我指着公告板问类加载器。

他回答我说:“这个板子只有一个,它叫做 ServletContext ,上面的是整个工程的配置信息,所有的Servlet都能看见这块板子"。

我点点头,然后被他带进了一个名字叫做 WEB-INF/classes 的房间中。

"接下来我要干点什么?"

"什么都不用做,等着就行了,会有人来找你的,我得走了,再见!"

第二章:来请求了

浑浑噩噩中,突然脑袋上方传来一个声音:"醒一醒!"

我被吓了一跳,连忙问道:“你是谁?”

“我是Web容器,有客户发请求了,想要访问你,现在把你的 init() 方法执行一下,这里有一张你的任务清单,每一个Servlet都会有一张,拿好了。"

我伸手拿过这张任务清单,上面的标题是:servletConfig ,上面已经详细的纪录了我的所有出生信息和初始化参数,但是最不能让我容忍的是,我居然有一个工作小名,叫abc。”

"没问题,但是我的 init() 方法里什么都没有,是不是就不用做什么了?"

"嗯对。"

Web容器话音未落,身后已经多了一个人,他对我说道:“你好,我是一个线程,我叫9527,是老大分配过来的。”

"你要做什么?"

"我需要把你的 service() 执行一下,正好我刚拿到老大创建的 requestresponse。对了,用户发送的是一个get请求,service() 方法会自动调用 doGet() ,并且把这两个参数传递给 doGet() 方法,然后我要执行它里面的内容!"

话音未落,突然身后又多了一个人,他对我说道:“你好,我是一个线程,我叫9528,是老大分配过来的。”

"又来一个?"

我回过身问9527怎么办,9527淡定地回复我说:“没关系,我们所有线程都可以同时工作,互相不干扰,因为我们谁都没有带锁,如果涉及到共享数据,我们可能会干一架,你在旁边看就好了。”

过了几分钟,两个人都把活干完了,并且他们的请求已经得到了正确的响应,然后他们先后对我说了再见,并且回到了线程池中,临走的时候9527还把那两个参数随手一扔,跟我说这两个参数已经没有什么用了,扔地上吧,一会儿GC大妈会过来处理。

第三章:关闭服务器

我的生活就在这里日复一日,直到有一天,头顶又传来那个讨厌的声音:“abc!服务器要重启了!请调用一下你的 destroy() 方法,留最后的遗言!”

"可是我的 destroy() 方法中没有任何代码.."

“哦,那你去死吧...”

然后我眼前一黑,大家再见。