Servlet的本质


作为一个Web开发者,Servlet是每天工作过程中都要打交道的老伙伴了。尽管现在随着Spring Boot的流行,几乎不需要再实现Servlet接口了,但是Spring Boot默认Web容器是Tomcat,Servlet仍然在我们看不见的地方发挥着的重要作用。谈到Servlet总是绕不开Servlet容器,生命周期,HTTP请求工作流程等字眼,在面试过程中也是常被追问三连,本文就聊一聊Servlet到底是个什么东西。

Servlet的本质

一个Servlet其实就是一个实现了Java特殊接口的类,它由支持Servlet(即能够执行Servlet的引擎)的WEB容器调用和启动运行。一个Servlet类负责处理它对应的一个或者多个URL地址的访问请求,接收客户端发出的访问请求信息和产生响应内容。

上面这段话就阐述了Servlet的本质,但是对于不了解它工作原理的人来说,这句话可能并不能给你带来什么实质性的理解。Servlet本质上就是一个接口(接口就是规范),这个接口定义的规范就是开发者和Servlet容器之间沟通的桥梁。

  • Servlet容器的作用:把网页的请求封装成Requset和Response对象,分发给对应的Servlet
  • Servlet的作用:开发者在实现Servlet接口的方法中编写业务代码

Servlet的特点

  1. Servlet其实就是一个供其他程序(Servlet引擎)调用的Java类,它不能独立运行,它的运行完全由Servlet引擎控制和调度;离开了Servlet引擎,Servlet就毫无能力了。
  2. Servlet引擎是一种容器程序,它负责管理和维护所有Servlet对象的生命周期。Servlet的加载、执行流程,以及如何接收客户端传过来的访问请求信息和如何将产生的响应内容传回给客户端等具体的底层事务,都通过Servlet引擎来实现。Servlet引擎负责将客户端的访问请求信息转交给Servlet程序,将Servlet程序产生的访问响应内容转交给客户端。简而言之,Servlet引擎就像是客户端与服务器端的Servlet程序打交道的桥梁。
  3. Servlet程序的运行过程其实就是它与Servlet引擎交互的过程,Servlet程序只和Servlet引擎打交道,它与WEB服务器和客户端没有任何的交互。
  4. Servlet属于一种插件,它是一个提供了一些约定方法供容器调用的类,它只负责在自身的方法中接收并处理容器传过来的数据,以及生成并返回给容器需要的数据和状态信息。
  5. Servlet最常见的应用在读取WEB浏览器传递给WEB服务器的请求信息并产生WEB服务器返回给WEB浏览器的响应内容(动态网页文档内容)。
  6. WEB服务器上可以部署多个Servlet程序,每个Servlet程序必须说明其能处理的URL请求,当收到符合的URL请求后,将由Servlet引擎去调用相应的Servlet程序处理请求。

Servlet 相关接口

我们首先来看一看Servlet接口中定义了哪些方法吧。

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

其中,init( ),service( ),destroy( )是Servlet生命周期的方法。代表了Servlet从“出生”到“工作”再到“死亡 ”的过程。Servlet容器(例如TomCat)会根据下面的规则来调用这三个方法

  1. init( ),当Servlet第一次被请求时,Servlet容器就会开始调用这个方法来初始化一个Servlet对象出来,但是这个方法在后续请求中不会在被Servlet容器调用,就像人只能“出生”一次一样。我们可以利用init( )方法来执行相应的初始化工作。调用这个方法时,Servlet容器会传入一个ServletConfig对象进来从而对Servlet对象进行初始化。
  2. service( )方法,每当请求Servlet时,Servlet容器就会调用这个方法。就像人一样,需要不停的接受老板的指令并且“工作”。第一次请求时,Servlet容器会先调用init( )方法初始化一个Servlet对象出来,然后会调用它的service( )方法进行工作,但在后续的请求中,Servlet容器只会调用service方法了。
  3. destory,当要销毁Servlet时,Servlet容器就会调用这个方法,就如人一样,到时期了就得死亡。在卸载应用程序或者关闭Servlet容器时,就会发生这种情况,一般在这个方法中会写一些清除代码。

ServletRequset接口

Servlet容器对于接受到的每一个Http请求,都会创建一个ServletRequest对象,并把这个对象传递给Servlet的Sevice( )方法。其中,ServletRequest对象内封装了关于这个请求的许多详细信息。我们来看一看ServletRequest接口的部分内容:

public interface ServletRequest {

    int getContentLength();//返回请求主体的字节数

    String getContentType();//返回主体的MIME类型

    String getParameter(String var1);//返回请求参数的值

}

其中,getParameter是在ServletRequest中最常用的方法,可用于获取查询HTTP请求传过来的值。

ServletResponse接口

javax.servlet.ServletResponse接口表示一个Servlet响应,在调用Servlet的Service( )方法前,Servlet容器会先创建一个ServletResponse对象,并把它作为第二个参数传给Service( )方法。ServletResponse隐藏了向浏览器发送响应的复杂过程。我们也来看看ServletResponse内部定义了哪些方法:

public interface ServletResponse {
    String getCharacterEncoding();

    String getContentType();

    ServletOutputStream getOutputStream() throws IOException;

    PrintWriter getWriter() throws IOException;

    void setCharacterEncoding(String var1);

    void setContentLength(int var1);

    void setContentType(String var1);

    void setBufferSize(int var1);

    int getBufferSize();

    void flushBuffer() throws IOException;

    void resetBuffer();

    boolean isCommitted();

    void reset();

    void setLocale(Locale var1);

    Locale getLocale();
}

其中的getWriter方法,它返回了一个可以向客户端发送文本的的Java.io.PrintWriter对象。默认情况下,PrintWriter对象使用ISO-8859-1编码(该编码在输入中文时会发生乱码)。

ServletConfig接口

当Servlet容器初始化Servlet时,Servlet容器会给Servlet的init( )方式传入一个ServletConfig对象参数。这个参数类型是个接口,里面是一些 在 web.xml 中对当前 Servlet类的配置信息。Servlet 规范将Servlet 的配置信息全部封装到了 ServletConfig 接口对象中。在tomcat调用 init()方法时,首先会将 web.xml 中当前 Servlet 类的配置信息封装为一个对象。这个对象的类型实现了ServletConfig 接口,Web 容器会将这个对象传递给init()方法中的 ServletConfig 参数。每一个servlet都对应一个ServletConfig用于封装各自的配置信息,即有几个servlet就会产生几个ServletConfig对象ServletConfig源码如下


public interface ServletConfig {
    String getServletName();
     // 获取到当前 Servlet 的上下文对象 ServletContext
    ServletContext getServletContext();

    String getInitParameter(String var1);

    Enumeration getInitParameterNames();
}

ServletContext接口

ServletContext对象表示Servlet应用程序。每个Web应用程序都只有一个ServletContext对象。在将一个应用程序同时部署到多个容器的分布式环境中,每台Java虚拟机上的Web应用都会有一个ServletContext对象。ServletContext对象包含Web应用中所有 Servlet 在 Web 容器中的一些数据信息。ServletContext随着Web应用的启动而创建,随着 Web 应用的关闭而销毁。一个 Web 应用只有一个ServletContext 对象。ServletContext中不仅包含了 web.xml 文件中的配置信息,还包含了当前应用中所有Servlet可以共享的数据。可以这么说, ServeltContext 可以代表整个应用,所以ServletContext有另外一个名称:application。

ServletContext中常用方法

ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,可以通过ServletConfig.getServletContext()方法获得ServletContext对象。

  • String getInitParameter ():获取 web.xml 文 件 的 中 指 定 名 称 的上下文参数值 。
  • Enumeration getInitParameterNames():获取 web.xml 文件的中的所有的上下文参数名称。其返回值为枚举类型 Enumeration。
  • void setAttribute(String name, Object object):在 ServletContext 的公共数据空间中,也称为域属性空间,放入数据。这些数据对于 Web应用来说,是全局性的,与整个应用的生命周期相同。当然,放入其中的数据是有名称的,通过名称来访问该数据。
  • Object getAttribute(String name):从 ServletContext 的域属性空间中获取指定名称的数据。
  • void removeAttribute(String name):从 ServletContext 的域属性空间中删除指定名称的数据。
  • String getRealPath(String path):获取当前 Web 应用中指定文件或目录在本地文件系统中的路径。
  • String getContextPath():获取当前应用在 Web 容器中的名称。

web.xml中的ServletContext配置信息

<!-- 初始化参数 -->
  <context-param>
      <param-name>MySQLDriver</param-name>
      <param-value>com.mysql.jdbc.Driver</param-value>
  </context-param>
  <context-param>
      <param-name>dbURL</param-name>
      <param-value>jdbc:mysql:</param-value>
  </context-param>

配置信息的使用

//创建servlet:
public class ContextTest01 implements Servlet {

    private ServletConfig config;

    @Override
    public void destroy() {

    }

    @Override
    public ServletConfig getServletConfig() {
        return this.config;
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        this.config = servletConfig;
    }

    @Override
    public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
        ServletContext application = this.config.getServletContext();
        System.out.println("ContextTest01:" + application);

        String driver = application.getInitParameter("MySQLDriver");
        System.out.println(driver);

        String contextPath = application.getContextPath();
        System.out.println("contextPath:" + contextPath);

        //文件在硬盘中的绝对路径
        String realPath = application.getRealPath("FirstServlet");
        System.out.println("realPath:" + realPath);

        //向ServletContext中添加属性
        application.setAttribute("admin", "tiger");
        application.setAttribute("password", 123456);
        //删除password
        application.removeAttribute("password");
    }

}

GenericServlet抽象类

前面我们编写Servlet一直是通过实现Servlet接口来编写的,但是,使用这种方法,则必须要实现Servlet接口中定义的所有的方法,即使有一些方法中没有任何东西也要去实现,并且还需要自己手动的维护ServletConfig这个对象的引用。因此,这样去实现Servlet是比较麻烦的。幸好,GenericServlet抽象类的出现很好的解决了这个问题。本着尽可能使代码简洁的原则,GenericServlet实现了Servlet和ServletConfig接口,下面是GenericServlet抽象类的具体代码:

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
    private transient ServletConfig config;

    public GenericServlet() {
    }

    public void destroy() {
    }

    public String getInitParameter(String name) {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameter(name);
        }
    }

    public Enumeration<String> getInitParameterNames() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameterNames();
        }
    }

    public ServletConfig getServletConfig() {
        return this.config;
    }

    public ServletContext getServletContext() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletContext();
        }
    }

    public String getServletInfo() {
        return "";
    }

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    public void init() throws ServletException {
    }

    public void log(String msg) {
        this.getServletContext().log(this.getServletName() + ": " + msg);
    }

    public void log(String message, Throwable t) {
        this.getServletContext().log(this.getServletName() + ": " + message, t);
    }

    public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    public String getServletName() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletName();
        }
    }
}

GenericServlet抽象类相比于直接实现Servlet接口,有以下几个好处

  1. 为Servlet接口中的所有方法提供了默认的实现,则程序员需要什么就直接改什么,不再需要把所有的方法都自己实现了。
  2. 将init( )方法中的ServletConfig参数赋给了一个内部的ServletConfig引用从而来保存ServletConfig对象,不需要程序员自己去维护ServletConfig了。

因此使用GenericServlet编写Servlet实现代码将会变得更加的简洁,程序员的工作也会减少很多。 然而,虽然GenricServlet是对Servlet一个很好的加强,但是也不经常用,因为他不像HttpServlet那么高级。HttpServlet才是主角,在现实的应用程序中被广泛使用。那么我们接下来就看看传说中的HttpServlet到底厉害在哪里吧。

HttpServlet抽象类

HttpServlet抽象类是继承于GenericServlet抽象类而来的。使用HttpServlet抽象类时,还需要借助分别代表Servlet请求和Servlet响应的HttpServletRequest和HttpServletResponse对象。HttpServletRequest接口扩展于javax.servlet.ServletRequest接口,HttpServletResponse接口扩展于javax.servlet.servletResponse接口。

public interface HttpServletRequest extends ServletRequest

public interface HttpServletResponse extends ServletResponse

HttpServlet抽象类覆盖了GenericServlet抽象类中的Service( )方法,并且添加了一个自己独有的Service(HttpServletRequest request,HttpServletResponse方法。我们来具体的看一看HttpServlet抽象类是如何实现自己的service方法吧。

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    try {
        request = (HttpServletRequest)req;
        response = (HttpServletResponse)res;
    } catch (ClassCastException var6) {
        throw new ServletException("non-HTTP request or response");
    }

    this.service(request, response);
}

我们发现,HttpServlet中的service方法把接收到的ServletRequsest类型的对象转换成了HttpServletRequest类型的对象,把ServletResponse类型的对象转换成了HttpServletResponse类型的对象。然后在调用了自己重写的另一个service方法,继续看一下这个service方法的实现

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    long lastModified;
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader("If-Modified-Since");
            if (ifModifiedSince < lastModified) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
    } else if (method.equals("HEAD")) {
        lastModified = this.getLastModified(req);
        this.maybeSetLastModified(resp, lastModified);
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
        this.doTrace(req, resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }

}

在service方法中没有任何的服务逻辑,但是却在解析HttpServletRequest中的方法参数,并调用以下方法之一:doGet,doPost,doHead,doPut,doTrace,doOptions和doDelete。这7种方法中,每一种方法都表示一个Http方法。doGet和doPost是最常用的。所以,如果我们需要实现具体的服务逻辑,不再需要覆盖service方法了,只需要覆盖doGet或者doPost就好了。

Servlet的工作流程

说了这么多,最后总结一下Servlet的工作流程吧,先上图

结合前文和这个图,我们应该能回答这些问题

1.什么是Servlet?

答:Servlet是一套用于给Servlet容器使用接口规范

2.什么是Servlet容器?

答:Servlet是一个遵循Servlet规范的Servlet执行引擎。它可以接收http请求,并调用对应的Servlet类处理请求,返回响应

3.不使用Servlet可以处理http请求吗?

答:可以,我们可以自己实现一个引擎来接收http请求,然后就可以不遵循Servlet规范,自己编写代码处理http请求了


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
RPC与gRPC框架 RPC与gRPC框架
RPC的语义是远程过程调用,在一般的印象中,就是将一个服务调用封装在一个本地方法中,让调用者像使用本地方法一样调用服务,对其屏蔽实现细节。而具体的实现是通过调用方和服务方的一套约定,基于TCP长连接进行数据交互达成。上面的解释似云里雾里,仅
2020-11-15
Next 
gitHub无法访问的问题 gitHub无法访问的问题
这两天github又访问不了,作为一名程序员github总是打不开,有时打开的很慢真的挺烦人的。在网上找了一堆ip和域名的地址映射放到hosts文件中发现也不行。访问github页面依然如下 原因分析因为github经常换域名和ip的映射
2020-10-24
  TOC