第 04 章 Servlet

第 04 章 Servlet

该部分的代码主要集中在servlet-jspdemo04中。

引入Servlet

JSP的初中就是了更加方便的进行视图展示。但是在作为页面又嵌入了大量的Java代码。

那么为了改善JSP的运行和开发模式:JSP只负责进行数据的展示,而Servlet用于负责进行业务的传递。那么就形成了下面的开发模式:

  • JSP,JSP本身就是一种负责显示视图的技术。
    • 接受请求,调用JavaBean去处理请求。
    • 显示处理后的数据
  • JavaBean
    • 封装数据
    • 执行业务逻辑

MVC

  1. 模型层Model:其实就是之前所提到的Dao以及Service。其代表的框架就有:jdbc、Mybatis、Hirbernat等等。
    1. Dao层,主要负责数据持久化,调用数据库
    2. Service层,主要负责业务数据的处理
  2. 视图层View:JSP(严格意义称为后台技术)以及VUE。进行数据的展示。负责视图的展示。
  3. 交互层Controller:也叫数据交互层,负责接收前端视图层传递的数据,交由数据模型层(交给Service层,再由Service层交递Dao层)来进行业务处理。
MVC

Servlet作用

  1. 本身不做任何业务处理。由Service进行数据处理。
  2. 只是接收请求并决定调用哪个JavaBean去处理请求。
  3. 确定用哪个页面来显示处理返回的数据。
Servlet作用

JavaWebApp生命周期[1]

JavaWeb 应用的生命周期是由 Servlet 容器来控制的。归纳起来,JavaWeb 应用的生命周期包括 3 个阶段。

  • 启动阶段:加载Web应用的有关数据,创建`ServletContext对象 ,对Filter和一些Servlet进行初始化。
  • 运行时阶段:为客户端服务。
  • 终止阶段:释放Web应用所占用的各种资源。

Servlet生命周期以及web.xml配置

下面将通过编写实现Servlet的方式讲解Servlet在各个生命周期中的变化。要实现Servlet的相关功能也就不得不讲解web.xmldemo04/src/main/webapp/WEB-INF/web.xml)的作用以及如何配置。

关于Servlet在web.xml中配置

与配置Servlet相关的标签有两个:

  1. <servlet></servlet>
  2. <servlet-mapping></servlet-mapping>

配置Servlet时,需要注意下面几点:

  1. <servlet></servlet><servlet-mapping></servlet-mapping>两个标签是成对出现的。
  2. 每一对<servlet></servlet><servlet-mapping></servlet-mapping>中的<servlet-name></servlet-name>必须一致
  3. 关于Url-pattern的配置方式
    1. 使用绝对路径
    2. 指定前缀,比如/test*,那么任何访问URL携带/testaaaaa还是/testbbbbb都将匹配到指定的Servlet类上。
    3. 使用通配符指定后缀,如*.do

比如,下面的xml配置简单实现了一个Servlet访问。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>ServletCreateMode1</servlet-name>
        <!-- 设置解析的Servlet类 -->
        <servlet-class>com.ermao.servlet.controller.ServletCreateMode1</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletCreateMode1</servlet-name>
        <url-pattern>/servlet</url-pattern>
    </servlet-mapping>
</web-app>

这里已经配置好了一个servlet。下面将通过访问servlet一窥生命周期。

Servlet生命周期概述

Servlet生命周期分为四个阶段:

  1. 加载和实例化;
  2. 初始化;
  3. 处理请求;
  4. 销毁;

下面将通过实现Servlet接口,讲解各个生命周期:

public class ServletCreateMode1 implements Servlet {
    @Override
    public void init (ServletConfig servletConfig) throws ServletException {
        System.out.println("Servlet初始化");
    }

    @Override
    public ServletConfig getServletConfig () {
        System.out.println("获取Servlet配置");
        return null;
    }

    @Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("业务被调用");
    }

    @Override
    public String getServletInfo () {
        System.out.println("获取Service信息");
        return null;
    }

    @Override
    public void destroy () {
        System.out.println("Servlet被销毁");
    }
}

从上面的代码中,我们可以看到实现Servlet接口时,必须实现的5个方法,不过先关注下面三个方法。

  1. init (ServletConfig servletConfig),Servlet被初始化和加载
  2. service (ServletRequest servletRequest, ServletResponse servletResponse)业务被调用
  3. destroy (),Servlet被销毁。
Servlet生命周期

加载和实例化

在请求抵达后,由容器加载完成。实例化过程既调用所编写的Servlet类的空参构造函数。

这里我们提供两个Servlet的代码;

public class ServletCreateMode1 implements Servlet {

    public ServletCreateMode1 () {
        System.out.println(this.getClass().getSimpleName()+"被实例化!");
    }

    @Override
    public void init (ServletConfig servletConfig) throws ServletException {
        System.out.println("【"+this.getClass().getSimpleName()+"】执行init()进行初始化!");
    }

    @Override
    public ServletConfig getServletConfig () {
        System.out.println("获取Servlet配置");
        return null;
    }

    @Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println(this.getClass().getSimpleName()+"执行业务代码service()");
    }

    @Override
    public String getServletInfo () {
        System.out.println("获取Service信息");
        return null;
    }

    @Override
    public void destroy () {
        System.out.println(this.getClass().getSimpleName()+"Servlet被销毁!");
    }
}

第二个Servlet代码如下:

public class ServletCreateMode2 extends GenericServlet {
    public ServletCreateMode2 () {
        System.out.println(this.getClass().getSimpleName() + "被实例化");
    }

    @Override
    public void init () throws ServletException {
        System.out.println("【"+this.getClass().getSimpleName() + "】执行init()进行初始化!");
    }

    @Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println(this.getClass().getSimpleName() + "执行业务代码service");
    }
    
        @Override
    public void destroy () {
        super.destroy();
        System.out.println(this.getClass().getSimpleName()+"Servlet被销毁!");
    }
}

下面则为web.xml的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <!-- /servlet -->
    <servlet>
        <servlet-name>ServletCreateMode1</servlet-name>
        <servlet-class>com.ermao.servlet.controller.ServletCreateMode1</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletCreateMode1</servlet-name>
        <url-pattern>/servlet</url-pattern>
    </servlet-mapping>

    <!-- /servlet2 -->
    <servlet>
        <servlet-name>ServletCreateMode2</servlet-name>
        <servlet-class>com.ermao.servlet.controller.ServletCreateMode2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletCreateMode2</servlet-name>
        <url-pattern>/genericservlet</url-pattern>
    </servlet-mapping>
</web-app>

在控制台中开启运行后,先将tomcat的启动的日志清除掉后,再观察控制台输出(分别访问相关Servlet的地址,这里使用谷歌和火狐浏览器分别访问对应地址)。得到下面的输出日志:

# 在谷歌浏览器中访问/servlet(第一次访问)
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
ServletCreateMode1执行业务代码service()
# 在谷歌浏览器中访问/genericservlet(第一次访问)
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode2执行业务代码service

# 下面两条日志则是在火狐浏览器中的访问结果(第2次以及第3次访问)
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service

从上面的结果中可以看出,满足我们之前的描述。当容器接受请求后,将加载和实例化对应Servlet。如果此时Servlet没有进行过初始化,那么对应的Servlet将执行各自的初始化方法init()

初始化

可结合上一节的内容(《加载和初始化》)理解整个生命周期。

在Servlet的生命周期中,仅执行一次init()方法,它是在服务器装入Servlet时执行的,可以配置服务器,以在启动服务器或客户机首次访问Servlet时装入Servlet。无论有多少客户机访问Servlet,都不会重复执行init()

只会初始化一次,实例被加载时。

当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。

处理请求

每次有请求抵达,都会触发业务处理。service()方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service()方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GETPOSTPUTDELETE等),并在适当的时候调用 doGetdoPostdoPutdoDelete 等方法。

下面是该方法的特征:

销毁

容器关闭,或者servlet被回收时,该方法将会被调用。这里将结合《加载和实例化》中的代码,进行演示。将tomcat关闭后。控制台输出结果如下所示。

# 在谷歌浏览器中访问/servlet(第一次访问)
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
ServletCreateMode1执行业务代码service()
# 在谷歌浏览器中访问/genericservlet(第一次访问)
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode2执行业务代码service

# 下面两条日志则是在火狐浏览器中的访问结果(第2次以及第3次访问)
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service


/Users/futianyu/Development.localized/Code.localized/WebServer/apache-tomcat-9.0.36/bin/catalina.sh stop
Disconnected from the target VM, address: '127.0.0.1:51324', transport: 'socket'
20-Jun-2020 20:14:56.527 信息 [main] org.apache.catalina.core.StandardServer.await 通过关闭端口接收到有效的关闭命令。正在停止服务器实例。
20-Jun-2020 20:14:56.527 信息 [main] org.apache.coyote.AbstractProtocol.pause 暂停ProtocolHandler["http-nio-8084"]
20-Jun-2020 20:14:56.538 信息 [main] org.apache.catalina.core.StandardService.stopInternal 正在停止服务[Catalina]
ServletCreateMode1Servlet被销毁!
ServletCreateMode2Servlet被销毁!
20-Jun-2020 20:14:56.549 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["http-nio-8084"]
20-Jun-2020 20:14:56.552 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毁协议处理器 ["http-nio-8084"]
Disconnected from server

从控制台的结构可以看出,当容器关闭后,各个Servlet将逐一执行destroy()方法,销毁各自的Servlet。

Servlet中web.xml配置

下面将讲解servlet中常用的一些配置信息。当然还有其他的配置,

web.xml配置总览

名称 描述 常用
display-name 定义了WEB应用的名字 T
description 声明WEB应用的描述信息 T
distributable 元素为空标签,它的存在与否可以指定站台是否可分布式处理.如果web.xml中出现这个元素,则代表站台在开发时已经 被设计为能在多个JSP Container 之间分散执行. F
context-param 声明应用范围内的初始化参数。在应用内共享. T
filter 过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。 T
filter-mapping 一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联。 T
listener servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。 T
servlet 在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。 T
servlet-mapping 定义了servlet与url之间的映射关系,其name与<servlet>元素相连. T
session-config 如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存。可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值。 T
mime-mapping 如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证。 F
welcome-file-list 欢迎页面(html,htm或jsp等) T
error-page 异常被抛出时,指定将要显示的页面。 T
jsp-config 用于为Web应用程序中的JSP文件提供全局配置信息。 它有两个子元素,taglib和jsp-property-group。 F
security-constraint 用于将安全约束与一个或多个Web资源集合相关联 F
login-config 指定服务器应该怎样给试图访问受保护页面的用户授权。它与sercurity-constraint元素联合使用。 T
security-role 定义安全角色.该定义包括对安全角色的可选描述以及安全角色名称。 F
resource-env-refType 声明与资源相关的一个管理对象。 F
resource-ref 声明一个资源工厂使用的外部资源。 F

下面的介绍,并不会全部介绍,其中只介绍了部分常用的web.xml配置。其他的可以在《参考资料》中提供的链接可以找到相关使用。

Servlet初始化参数配置

<!-- /servlet -->
<servlet>
    <servlet-name>ServletCreateMode1</servlet-name>
    <servlet-class>com.ermao.servlet.controller.ServletCreateMode1</servlet-class>
    <!-- 初始化配置 -->
    <init-param>
        <param-name>charSetContent</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>ServletCreateMode1</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

通过设置<init-param></init-param>设置对应的Servlet初始化的参数,其可设置的一般有下面两个标签:

  1. <param-name></param-name>设置参数名称,英文;
  2. <param-value>UTF-8</param-value>设置参数指,也是英文。

设置完成后,可以通过init(ServletConfig servletConfig)中的参数servletConfig获取在web.xml中设置的参数信息,比如下面的代码:

public void init(ServletConfig config) throws ServletException {
    String initParam=config.getInitParameter("charSetContent");
    System.out.println(initParam);
}

其次,设置的初始化参数只对所设置的servlet有效,对未设置的servlet则无效,其他的servlet无法获取到。比如:ServletA设置了编码格式,那么ServletB则无法获取到ServletA的初始化参数值,但是ServletA的子类可以获取到。

通过配置实现Servlet的加载顺序

<servlet></servlet>下面还有一个参数<load-on-startup></load-on-startup>,是用于控制,Servlet的加载顺序的。如果没有设置,那么容器将按照自定义的顺序进行加载(当请求抵达容器后,加载初始化对应的Servlet类)。

<servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.ermao.demo.MyServlet</servlet-class>

    <load-on-startup>1</load-on-startup>
</servlet>

<load-on-startup>1</load-on-startup>中的数字指明servlet加载顺序,数字小的先加载。如果值为负或未指定,web容器可以在任何时候加载servlet。

下面通过一个例子来看下其加载的顺序变化:

<!-- /servlet -->
<servlet>
    <servlet-name>ServletCreateMode1</servlet-name>
    <servlet-class>com.ermao.servlet.controller.ServletCreateMode1</servlet-class>
    <!-- 初始化配置 -->
    <init-param>
        <param-name>charSetContent</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>ServletCreateMode1</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

<!-- /genericservlet -->
<servlet>
    <servlet-name>ServletCreateMode2</servlet-name>
    <servlet-class>com.ermao.servlet.controller.ServletCreateMode2</servlet-class>
    <load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>ServletCreateMode2</servlet-name>
    <url-pattern>/genericservlet</url-pattern>
</servlet-mapping>

<!-- /httpservlet -->
<servlet>
    <servlet-name>ServletCreateMode3</servlet-name>
    <servlet-class>com.ermao.servlet.controller.ServletCreateMode3</servlet-class>
    <load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>ServletCreateMode3</servlet-name>
    <url-pattern>/httpservlet</url-pattern>
</servlet-mapping>

当容器启动后,控制台输出日志如下:

# 容器启动后,随机分别加载了三个Servlet并执行了初始化。
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
UTF-8
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode3被实例化!
【ServletCreateMode3】执行init()进行初始化!
[2020-06-21 02:41:44,476] Artifact demo04:war exploded: Artifact is deployed successfully
[2020-06-21 02:41:44,476] Artifact demo04:war exploded: Deploy took 618 milliseconds

ServletContext配置

ServletContext也就是我们JSP中说的Application作用域,ServletContext中配置的参数,不属于任何一个Servlet,而是属于整个webapp的。由于ServletContext属于webapp,所以在层次上与<servlet>标签属于同一层。

<!-- 配值application作用域 -->
<context-param>
    <!-- 参数注释 -->
    <description>访问初始化值</description>
    <!-- 参数名称 -->
    <param-name>visitTimes</param-name>
    <!-- 参数值 -->
    <param-value>0</param-value>
</context-param>
<context-param>
    <description>登录初始化值</description>
    <param-name>loginTimes</param-name>
    <param-value>0</param-value>
</context-param>

仍然需要注意的一点是:<param-name>loginTimes</param-name>必须唯一。如何获取整个Servlet的配置信息?

@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Enumeration<String> names = getServletConfig().getServletContext().getInitParameterNames();
    while (names.hasMoreElements()) {
        String name = names.nextElement();
        System.out.println(name+":"+getServletConfig().getServletContext().getInitParameter(name));
    }
}

通过获取ServletContext上下文,getServletConfig().getServletContext().getInitParameterNames()来获取整个webapp的初始化参数值。

Session会话超时配置

会话超时配置的时间单位为:分钟!

<session-config> 
    <session-timeout>120</session-timeout> 
</session-config> 

配置错误页面

配置错误页面有两种方式,一种是通过定义<error-code></error-code>来定义错误错误页面;另一种是通过定义通过异常的类型<exception-type></exception-type>来定义错误错误页面。

第一种,当系统发生404错误时,跳转到错误处理页面NotFound.jsp。定义方式:

<error-page> 
    <error-code>404</error-code> 
    <location>/NotFound.jsp</location> 
</error-page> 

第二种,通过异常的类型配置error-page,下面面配置了当系统发生java.lang.NullException(即空指针异常)时,跳转到错误处理页面error.jsp

<error-page> 
    <exception-type>java.lang.NullException</exception-type> 
    <location>/error.jsp</location> 
</error-page> 

其他配置

<display-name></display-name>

用于定义项目名称。

<description></discription>

用于定义项目的描述信息。

<icon></icon>

<icon></icon>下面有两个子标签

  • <small-icon></small-icon>标签内值为/路径/image.gif。small-icon元素应指向web站台中某个小图标的路径,大小为16 X 16 pixel,但是图象文件必须为GIF或JPEG格式,扩展名必须为:.gif.jpg
  • <large-icon></large-icon>
<mime-mapping></mime-mapping>

mime-mappingweb.xml中的一个节点,用来指定对应的格式的浏览器处理方式。如下所示:

<mime-mapping>
    <extension>html</extension>
    <mime-type>text/html;charset=UTF-8</mime-type>
</mime-mapping>
<welcome-file-list></welcome-file-list>

<welcome-file-list></welcome-file-list>设置的是欢迎页面的列表。

<welcome-file-list> 
    <welcome-file>index.jsp</welcome-file> 
    <welcome-file>index.html</welcome-file> 
    <welcome-file>index.htm</welcome-file> 
</welcome-file-list> 

访问一个网站时,默认看到的第一个页面就叫欢迎页,一般情况下是由首页来充当欢迎页的。一般情况下,我们会在web.xml中指定欢迎页

如果指定了多个欢迎页面,显示时按顺序从第一个找起,如果第一个存在,就显示第一个,后面的不起作用。如果第一个不存在,就找第二个,以此类推。

访问一个网站时,默认看到的第一个页面就叫欢迎页,一般情况下是由首页来充当欢迎页的。一般情况下,我们会在web.xml中指定欢迎页。但web.xml并不是一个Web的必要文件,没有web.xml,网站仍然是可以正常工作的。只不过网站的功能复杂起来后,web.xml的确有非常大用处,所以,默认创建的动态web工程在WEB-INF文件夹下面都有一个web.xml文件。默认去项目路径下寻找.jsp或者.html文件

Servlet作用域

在讲解JSP时,提到了有4个作用域,他们分别是:HttpServletRequestHttpSessionapplication(其本质是ServletContext)以及PageContext,这四个作用域。我们可以通过Tomcat所编译的JSP文件的代码可以看出在Servlet中如何获取相关作用域。因为Servlet属于非视图层,所以这里无法查看PageContext。故我们主要看下Servlet中如何获取其他三个作用域:HttpServletRequestHttpSessionapplication(其本质是ServletContext)。

下面给出的是JSP编译生成的java文件。我们主要看_jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)方法。

public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {
      
    ... 
    
    
    // 定义所使用到的变量:request,session,application,pageContext;
    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;
    
    
    try {
        response.setContentType("text/html;charset=UTF-8");
        pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);
        _jspx_page_context = pageContext;
        application = pageContext.getServletContext();
        config = pageContext.getServletConfig();
        session = pageContext.getSession();
        out = pageContext.getOut();
        _jspx_out = out;
        ....
      
    } catch (java.lang.Throwable t) {
        ....
    } finally {
        _jspxFactory.releasePageContext(_jspx_page_context);
    }
}

从上面的代码中,我们可以看出:

  1. request就是HttpServletRequest
  2. session就是HttpSession
  3. application就是ServletContext

那么在JSP中应该如何获取上面的作用域?从下面的方法service()中,我们可以看出一些端倪:

public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

    
}

servletRequestservletResponse与JSP中的HttpServletRequestHttpServletResponse很像。但是,ServletRequestServletResponse是无法满足我们的开发需求,我们查看下其继承关系如下:

  • HttpServletRequest是继承自ServletRequest
  • HttpServletResponse是继承自ServletResponse

那么可以通过下面的代码,直接进行强转HttpServletRequestHttpServletResponse。从而获取到对应的其他三个作用域,以及请求的其他信息:


@Override
public void init (ServletConfig servletConfig) throws ServletException {
    System.out.println("【"+this.getClass().getSimpleName()+"】执行init()进行初始化!");
    // 获取编码设置
    charSetContent = servletConfig.getInitParameter("charSetContent");
    // 将配置信息存储到servletConfig中。
    this.servletConfig = servletConfig;
    System.out.println(charSetContent);
}

@Override
public ServletConfig getServletConfig () {
    System.out.println("获取Servlet配置");
    return servletConfig;
}

@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    System.out.println(this.getClass().getSimpleName()+"执行业务代码service()");
    // 作用域获取:Servlet中只能获取到三个作用域,他不像JSP可以获取到4个作用域
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    HttpSession session = httpServletRequest.getSession();
    ServletContext application = getServletConfig().getServletContext();
    
    // 获取Response相应输出流对象
    HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

    // 获取Http请求基本信息
    System.out.println("=================== 获取Http请求基本信息 ===================");
    // 获取请求方法
    System.out.println(httpServletRequest.getMethod());
    // 获取请求虚拟路径
    System.out.println(httpServletRequest.getRequestURI());
    // 获取请求全资源路径URL
    System.out.println(httpServletRequest.getRequestURL());


    // 获取客户端的cookie
    System.out.println("=================== 获取请求的参数:COOKIE ===================");
    Cookie[] cookies = httpServletRequest.getCookies();
    // 遍历cookie
    for (Cookie cookie:cookies) {
        System.out.println("获取cookie的生命周期:" + cookie.getMaxAge());
        System.out.println("获取cookie名字:" + cookie.getName());
        System.out.println("获取cookie指:" + cookie.getValue());
        System.out.println(System.lineSeparator());
    }

}

Servlet的三种创建方式

通过实现Servlet接口的了解,可以更好的了解GenericServlet以及HttpServlet作用

实现Servlet接口

时所有Java Servlet的基础接口类,规定了必须由Servlet具体类实现的方法集。

public class ServletCreateMode1 implements Servlet {

    private ServletConfig servletConfig;

    @Override
    public void init (ServletConfig servletConfig) throws ServletException {
        // Servlet初始化
        this.servletConfig = servletConfig;
    }

    @Override
    public ServletConfig getServletConfig () {
        // 获取该Servlet的配置信息。
        return servletConfig;
    }

    @Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 执行业务代码。
    }

    @Override
    public String getServletInfo () {
        // 获取Servlet作者信息
        return null;
    }

    @Override
    public void destroy () {
        // 销毁Servlet时,需要做什么。
    }
}

其中getServletConfig()getServletInfo()其实实际开发中用的相对较少。

  • getServletConfig()可以其子类通过super.getServletConfig()来获取其配置信息。比如我们所设置的编码格式等等。当然,需要在初始化阶段(init(ServletConfig servletConfig))将配置信息赋值给其属性。

    • ServletConfig 包含了servlet的初始化参数信息。
  • getServletInfo()一般是获取作者信息,如:由谁创建的Servlet,通过HttpServletResponse输出出去。这两个方法属于可选方法。

  • init():Servlet的初始化方法,仅仅会执行一次(已经多次讲到)

  • service():处理请求和生成响应,其中ServletRequest以及ServletResponse其作用如下:

    • ServletRequest
      • 封装客户的请求信息
      • 作用相当于JSP内置对象request
    • ServletResponse
      • 创建响应信息,将处理结果返回给客户端
      • 作用相当于JSP内置对象response
  • destroy():在服务器停止并且程序中的Servlet对象不再使用的时候调用,只执行一次

继承GenericServlet

Servlet的通用版本,是一种与协议无关的Servlet。任何情况下能使用。

public class ServletCreateMode2 extends GenericServlet {
    @Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

    }
}

继承HttpServlet

GenericServlet基础上扩展的基于Http协议的Servlet

public class ServletCreateMode3 extends HttpServlet {
    @Override
    protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doGet(req, resp);
        System.out.println("do Get");
    }

    @Override
    protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doPost(req, resp);
        System.out.println("do Get");
    }

    @Override
    protected void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.service(req, resp);
        System.out.println("do Service");
    }
}

通过上面的代码,可以观察下,其是如何生效的。

  • 当这三个方法都存在时,Servlet将默认调用service()方法。
  • service()方法没有被重写时,如果请求时GET那么将默认调用doGet()方法。同理,当请求方法为POST时,那么将默认掉哟个doPost()方法。

其实我们也可以通过HttpServlet中的service()方法,一看究竟。

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {

    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

从上面的代码中,我们也可以看出,其实现的原理是什么,在HttpServlet抽象类中service()根据不同请求调用不同处理的方法(通过多态实现)。

在重写HttpServlet的方法过程中,则不用调用HttpServlet中的方法。因为会直接跑错。比如,HttpServlet中的doGet()方法:

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
{
    String protocol = req.getProtocol();
    String msg = lStrings.getString("http.method_get_not_supported");
    if (protocol.endsWith("1.1")) {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    }
}
    
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    
    if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
        doGet(req, resp);
    } else {
        NoBodyResponse response = new NoBodyResponse(resp);
        doGet(req, response);
        response.setContentLength();
    }
}

Servlet输出

前面讲到了如何解析数据,包括如何设置web.xml。那么关键的是,业务处理完成后,因该如何输出响应信息?

使用PrintWrite输出内容

调用getWriter()将会返回一个PrintWriter对象,Servlet 用它来输出字符串像是的正文数据。

@Override
    public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        // 输出作用域的值web.xml中配置的Context值
        httpServletResponse.setContentType("text/html;charset:utf-8");
        httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
        Enumeration<String> names = this.servletConfig.getInitParameterNames();
        PrintWriter writer = httpServletResponse.getWriter();
        while (names.hasMoreElements()){
            String name = names.nextElement();
            writer.write(name + ":" + this.servletConfig.getInitParameter(name)+ System.lineSeparator());
        }
        writer.flush();
        writer.close();
    }

使用ServletOutputStream输出内容

getOutputStream():返回一个 ServletOutputStream 对象,Servlet用它来输出二进制的正文数据。

@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // super.doGet(req, resp);
    System.out.println("do Get");
    // 使用 outputStream 发送信息
    resp.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
    resp.setContentType("text/html;charset=UTF-8");

    ServletOutputStream outputStream = resp.getOutputStream();
    Enumeration<String> names = getServletConfig().getServletContext().getInitParameterNames();
    while (names.hasMoreElements()) {
        String name = names.nextElement();
        outputStream.write((name+":"+getServletConfig().getServletContext().getInitParameter(name)).getBytes(StandardCharsets.UTF_8.displayName()));
    }
    
    String outStr = "你好!";
    outputStream.write(outStr.getBytes(StandardCharsets.UTF_8.displayName()));
    outputStream.flush();
    outputStream.close();
}

关于输出对象ServletResponseHttpServletResponse

ServletResponse 中响应正文的默认 MIME 类型为 text/plain,即纯文本类型。而 HttpServletResponse 中响应正文的默认 MIME 类型为 text/html,即HTML文档类型。

注意事项

关于输出响应编码设置

关于输出响应设置编码格式由两个部分组成:

  • HttpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.displayName());仅仅是发送的浏览器的内容是UTF-8编码的,至于浏览器是用哪种编码方式显示不管。 所以当浏览器的显示编码方式不是UTF-8的时候,就会看到乱码,需要手动再进行一次设置。
  • HttpServletResponse.setContentType("text/html;charset=UTF-8");不仅发送到浏览器的内容会使用UTF-8编码,而且还通知浏览器使用UTF-8编码方式进行显示。设置响应头的Content-Type

<font style="color:red;font-weight:bolder">
这两种方式都需要在response.getWriter()或者response.getOutputStream()调用之前执行才能生效。如果,单设置其中一个,仍然会看到乱码!
</font>

输出响应时机

为了提高输出数据的效率,ServletOutputStreamPrintWriter先把数据写到缓冲区内。当缓冲区内的数据被提交给客户后,ServletResponseisCommitted() 方法返回 true。在以下几种情况下,缓冲区内的数据会被提交给客户,即数据被发送到客户端:

  • 当缓冲区内的数据已满时,ServletOutputStreamPrintWriter 会自动把缓冲区内的数据发送给客户端,并且清空缓冲区。
  • Servlet 调用 ServletResponse 对象的 flushBuffer() 方法。
  • Servlet 调用 ServletOutputStreamPrintWriter 对象的 flush() 方法或 close() 方法。

为了确保 ServletOutputStreamPrintWriter 输出的所有数据都会被提交给客户,比较安全的做法是在所有数据都输出完毕后,调用 ServletOutputStreamPrintWriterclose() 方法[1]

也就是说,如果需要提前将信息输出到客户端则可以调用PrintWriterclose()或者ServletResponse 对象的 flushBuffer() 。但是,如果未确定是否将信息已经返回给客户端时,可以调用ServletResponseisCommitted()判断下。

关于输出缓冲区

在《输出响应时机》一节中提到了关于输出响应的缓冲区,下面需要引入关于缓冲区相关操作的API。

  • int size = HttpServletResponse.getBufferSize(); 返回当前缓冲区的大小,单位是B(字节),默认情况下是8192,即8KB大小
  • HttpServletResponse.setBufferSize(16*1024);设置缓冲区的大小,单位B(字节)

设置了缓冲区大小后,将在全局webapp内生效!

练习

完成一个简单的请求分发,上面的学习过程中,我们已经知道了,当需要访问一个Servlet时,我们需要在web.xml配置Servlet。但是如果有100个Servlet,那么就需要在web.xml配置100个Servlet。岂不是做了很多的重复工作?

所以通过练习实现一个简单的请求分发(路由功能),将对应的URI转发到指定的Servlet中去。简单的实现将在demo05中查看。

参考资料

  1. 《web.xml中的servlet配置》,作者:奇客谷教程,发布时间:2019年3月1日
  2. 《JavaWeb-web.xml》,作者:舒山,发布时间:2019年3月1日
  3. 《Servlet 3.0 新特性详解》,作者:张建平,发布时间:2010年4月23日
  4. 《Servlet 4.0 入门》,作者:Alex Theedom,发布时间:2018年5月29日
  5. 《Java Web 扫盲行动》,作者:
    你在我家门口,发布时间:2019年04月26日

  1. 引用自《Java Web 扫盲行动》,详情查询请查看参考资料中链接。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352