美文网首页Java
第 04 章 Servlet

第 04 章 Servlet

作者: 傅二毛 | 来源:发表于2020-06-22 00:46 被阅读0次

    第 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 扫盲行动》,详情查询请查看参考资料中链接。

    相关文章

      网友评论

        本文标题:第 04 章 Servlet

        本文链接:https://www.haomeiwen.com/subject/zqltfktx.html