第 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相关的标签有两个:

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

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

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



    
        ServletCreateMode1
        
        com.ermao.servlet.controller.ServletCreateMode1
    
    
        ServletCreateMode1
        /servlet
    

这里已经配置好了一个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的配置文件:



    
    
        ServletCreateMode1
        com.ermao.servlet.controller.ServletCreateMode1
    
    
        ServletCreateMode1
        /servlet
    

    
    
        ServletCreateMode2
        com.ermao.servlet.controller.ServletCreateMode2
    
    
        ServletCreateMode2
        /genericservlet
    

在控制台中开启运行后,先将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与元素相连. 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初始化参数配置



    ServletCreateMode1
    com.ermao.servlet.controller.ServletCreateMode1
    
    
        charSetContent
        UTF-8
    


    ServletCreateMode1
    /servlet

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

  1. 设置参数名称,英文;
  2. UTF-8设置参数指,也是英文。

设置完成后,可以通过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类)。


    myServlet
    com.ermao.demo.MyServlet

    1

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

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



    ServletCreateMode1
    com.ermao.servlet.controller.ServletCreateMode1
    
    
        charSetContent
        UTF-8
    
    1


    ServletCreateMode1
    /servlet




    ServletCreateMode2
    com.ermao.servlet.controller.ServletCreateMode2
    2


    ServletCreateMode2
    /genericservlet




    ServletCreateMode3
    com.ermao.servlet.controller.ServletCreateMode3
    3


    ServletCreateMode3
    /httpservlet

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

# 容器启动后,随机分别加载了三个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,所以在层次上与标签属于同一层。



    
    访问初始化值
    
    visitTimes
    
    0


    登录初始化值
    loginTimes
    0

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

@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Enumeration 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会话超时配置

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

 
    120 
 

配置错误页面

配置错误页面有两种方式,一种是通过定义来定义错误错误页面;另一种是通过定义通过异常的类型来定义错误错误页面。

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

 
    404 
    /NotFound.jsp 
 

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

 
    java.lang.NullException 
    /error.jsp 
 

其他配置

用于定义项目名称。

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

下面有两个子标签

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

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


    html
    text/html;charset=UTF-8

设置的是欢迎页面的列表。

 
    index.jsp 
    index.html 
    index.htm 
 

访问一个网站时,默认看到的第一个页面就叫欢迎页,一般情况下是由首页来充当欢迎页的。一般情况下,我们会在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 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 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


这两种方式都需要在response.getWriter()或者response.getOutputStream()调用之前执行才能生效。如果,单设置其中一个,仍然会看到乱码!

输出响应时机

为了提高输出数据的效率,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)