Servlet的本质与工作原理

一、Servlet 概述

Servlet 类是 JavaWeb 的三大组件之一,它属于动态资源。其作用是处理请求,服务器通常会把接收到的请求交由 Servlet 的实现类来处理,通常Servlet的实现类需要完成:

  1. 接收请求数据;
  2. 处理请求;
  3. 完成响应。

例如:当客户端发出登陆请求,或者输出注册请求时,这些请求都会交由一个 Servlet 来处理。
Servlet 实现类中的方法需要由我们自己来实现,且每个 Servlet 实现类必须实现javax.servlet.Servlet接口,之后由服务器来创建 Servlet 类对象并调用相应的方法。

二、Servlet 的本质

Servlet并不是一个很复杂东西,事实上 Servlet 就是一个Java接口:

public interface Servlet {

    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

而我们知道,Java中的接口常被我们视为规范的存在,所以 Servlet 本身的作用也是作为一种规范存在。它并不能独立运行,我们所要实现的功能必须交由它的实现类实现并最终将实现类放置在一个 Servlet容器(例如Tomcat、Jetty)中运行。

Servlet 接口定义的就是一套处理网络请求的规范,所有实现 servlet 的类,都需要实现它接口中所定义的方法。其中包含了两个生命周期方法init()destory(),还有一个处理请求的service()

1. Servlet 的生命周期

Servlet是运行在Servlet容器中的,由Servlet容器来负责Servlet实例的查找、创建以及整个生命周期的管理。Servlet整个生命周期可以分为四个阶段:

  • (1) 类装载以及实例创建阶段

    默认情况下,Servlet实例是在接收到第一个请求时进行创建,并且在以后的请求中对这个实例进行复用。如果有Servlet实例需要进行一些复杂的操作(如打开文件,初始化网络连接等),可以配置在服务器启动时就创建实例。具体配置方法为:在web.xml的声明Servlet标签中添加<load-on-startup>1</load-on-start-up>标签。

  • (2) 实例初始化阶段

    一旦Servlet实例被创建,将会调用Servlet中的init(ServletConfig arg)方法,传入ServletConfig,即Servlet的相关配置信息,init()方法在整个Servlet的生命周期中只会被调用一次。

  • (3) 服务阶段

    实例初始化后,一旦由客户端请求,Servlet 就会调用service(ServletRequest req, ServletRespose res)方法处理数据并响应数据。

  • (4) 实例销毁阶段

    当Servlet容器决定销毁某个Servlet时,将会调用 Servlet 实例中的destory()方法,在destory()方法中进行资源释放。一旦Servlet实例的 destory() 方法被调用,Servlet 容器将不会发任何请求给这个Servlet实例,若 Servlet 容器需要再次使用这个 Servlet,需要重新实例化该 Servlet 实例。

需要注意的是:
在每一个应用中,每个 Servlet 只能拥有一个实例。
对于每一个Servlet实例,也有一个封装了对应配置的ServletConfig对象。
对于每一个应用程序,Servlet容器还会创建一个ServletContext对象,这个对象中封装了应用环境(上下文)的数据详情,每个应用程序也只有一个ServletContext。

2. Servlet 的工作流程

Servlet 是一个规范,但是我们实现完 Servlet 后并不能直接处理请求,Servlet 并不会直接与客户端打交道

那请求怎么来到 Servlet 的呢?答案是 Servlet 容器。比如我们最常用的Tomcat,我们实现的Servlet最终都要部署到Tomcat中,否则压根就不起作用。下面我们简单了解一下 Servlet部署到容器后的工作流程:

  1. Web服务器接收 http 请求后将请求移交给 Servlet容器;
  2. Servlet 容器对请求的 URL 进行解析并根据web.xml配置文件找到处理该应请求的Servlet,同时将 request、response对象一并传递给该类;
  3. Servlet 根据 request 对象可以得到客户端发过来的数据并做出对应的处理,之后将需要返回的信息放入 response 对象中并返回到客户端;
  4. Servlet 一旦处理完请求,Servlet 容器就会刷新 response 对象,并把控制权重新移交回给 web服务器。

所以 Tomcat 才是与客户端直接打交道的家伙,他监听了端口,请求过来后,根据url等信息,确定要将请求交给哪个servlet去处理,然后调用那个Servlet 的 service() 方法,service()方法返回一个 response对象,tomcat再把这个 response返回给客户端。

三、如何编写一个Servlet类

我们常见的实现 Servlet 的方式有以下三种

  1. 实现 javax.servlet.Servlet 接口;
  2. 继承 javax.servlet.GenericServlet 类;
  3. 继承 javax.servlet.http.HttpServlet 类;(一般使用这种方式,方便!)

1. 实现Servlet接口

代码演示:

public class AServlet implements Servlet {

  @override
  public void init(ServletConfig config) throws ServletException {
    System.out.println("init()...")
  }
  
  @override
  public void service(ServletRequest req, ServletRespose res) throws ServletException,IOException {
    System.out.println("service()...")
  }
  
  @override
  public void destory() {
    System.out.println("destory()...")
  }
  
  @override
  public ServletConfig getServletConfig() {
    return null;
  }
  
  @override
  public String getServletInfo() {
    return "";
  }
}

编写完代码后需要在web.xml中正确配置映射关系。之后启动服务器,再多次访问Servlet,console中输出以下信息:

接下来关闭Servlet容器,控制台打印:

注:Servlet中的方法实现后并不由我们自己来调用,而是由服务器来调用并做出反应。

2. 继承 GenericServlet/HttpServlet 类

我们知道浏览器最基本的请求有两种,Get/Post。如果我们采用上面所述直接实现接口的方式,那么我们必须这么写:

public void service(Requst request, Response response) {
    String method = request.getMethod();
    
    if (METHOD_GET.equals(method)) { // 如果是Get请求
        // 调用 service 层实现业务逻辑...
    } else if (METHOD_POST.equals(method)) { // 如果是Post请求
        // 调用 service 层实现业务逻辑...
    } else if (...) { // 其他类型的请求
        
    }
}

上面只是简单的一个小例子,但是我们可以从中看出:直接实现 Servlet 接口的效率并不高。所以我们可以来看看有没有办法来简化我们这些操作。

2.1 GenericServlet抽象类

首先我们找到一个 GenericServlet 的抽象类,该类的源代码如下:

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable{
  
    private static final long = 1L;
    privare transient ServletConfig config;
  
    public GenericServlet() { }
  
    @override
    public void destory() {}
  
    @override
    public Enumeration<String> getInitParameterNames() {
        return getServletConfig().getInitParameterNames();
    }
  
    @override
    public ServletConfig getServletContext(){
        return config;
    }
  
    @override
    public ServletContext getServletContext(){
        return getServletConfig().getServletContext();
    }
  
    @override
    public String getServletInfo(){
        return "";
    }
  
    @override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        init();
    }
  
    public void init() throws ServletException { }
  
    public void log(String msg) {
        getServletContext().log(getServletName() + ":" + message, t);
    }
  
    @override
    public abstract void service(ServletRequest req, ServletResponse res) throws ServletExcrption, IOException;
  
    @override
    public String getServletName() {
        return config.getServletName();   
    }
}

从上面代码我们可以发现 GenericServlet 做了如下改良:

  • 提升了 init() 方法中原本是形参的 servletConfig 对象的作用域(成员变量),方便其他方法进行使用
  • 定义了 init() 空参方法,继承该类的子类可以覆盖该方法,方便在创建 Servlet 时进行一些初始化操作
  • 还实现了 ServletConfig接口,可以直接调用getInitParameter()getServletContext() 等接种定义的方法。

但是我们也发现改抽象类并没有实现 service() 方法,也就说该类所定义的方法并不满足我们刚所提到的简化。

2.2 HttpServlet 抽象类

HttpServlet继承了

public abstract class HttpServlet extends GenericServlet {
    
    // ...其他代码
    
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
            this.service(request, response);
        } else {
            throw new ServletException("non-HTTP request or response");
        }
    }

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String method = req.getMethod();
            long lastModified;
            if (method.equals("GET")) {
                // ... 请求 doGet()方法
            } else if (method.equals("HEAD")) {
                // ... 请求 doHead() 方法
            } else if (method.equals("POST")) {
                // ... 请求 doPost() 方法
            } else if (method.equals("PUT")) {
                ...
                
        }
    
    // ...其他代码
}

从上面这段代码我们可以看到,HttpServlet 抽象类中已经帮我们实现了复杂的请求判断。所以我们在实现一个Servlet时,可以直接继承HttpServlet抽象类以便更加快捷地完成我们的业务逻辑(虽然现在应该没人直接把业务写Servlet里啦...)。

四、Servlet接口相关类型

在Servlet接口中还存在三个我们不熟悉的类型:

  • ServletConfig:init() 方法中的参数,它vlet配置对象,它对应Servlet的配置信息,对应 web.xml 文件中的<servlet>元素。
  • ServletRequest:service() 方法的参数,他表示请求对象,它封装了所有与请求相关的数据,它是由服务器创建的。
  • ServletResponse:service() 方法的参数,它表示响应对象,在Servlet()方法中完成对客户端的响应需要这个对象。

1. ServletRequest 和 ServletResponse

ServletRequest 和 ServletResponse 是 service() 方法中的两个参数,一个请求对象,一个响应对象,可以从ServletRequest对象中获取请求数据,可以使用ServletResponse对象完成响应。
ServletRequest和ServletResponse的实例由服务器创建,然后传递给
service()方法。如果在service()方法中希望使用HTTP相关功能,那么可以把ServletRequest和ServletResponse强转为HttpServletRequest和HttpServletResponse。这也说明我们经常需要在service()方法中对ServletRequest和ServletResponse进行强转,不过上面提到的HttpServlet已经解决了这个问题。

2. ServletConfig

ServleteConfig对象对应web.xml文件中的<servlet>元素。例如你想获取当前Servlet对象在web.xml文件中的配置名,那么可以使用ServletConfig.getServletName()方法获取!
ServletConfig对象是由服务器创建的,然后传递给Servlet的init()方法,可以在init()方法中使用它!

-String getServletName():获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称;
-ServletContext getServletContext():用来获取ServletContext对象。
-String getInitParameter(String name):用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值;
Enumeration getInitParameter(String name):用来获取在web.xml中配置的所有初始化参数名称。

五、ServletContext

一个项目中只有一个ServletContext对象!

1. ServletContext概述

服务器会为每个应用创建一个ServletContext对象:

  • ServletContext对象的创建是在服务器启动时完成的;
  • ServletContext对象的销毁是在服务器关闭时完成的。
  • ServletContext对象的作用是在整个Web应用的动态资源之间共享数据!
    例如在AServlet中向ServletContext对象中保存了一个值,然后在BServlet中就可以获得这个值,这就是共享数据了。

2. 获取ServletContext

Servletconfig#getServletContext();

GenericServlet#getServletContext();

HttpSession#getServletContext();

ServletContextEvent#getServletContext().

public class MyServlet implement Servlet {
  public void init(ServletConfig config) {
    ServletContext context = config.getServletContext();
  }
  ......
}

public class MyServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse res) {
    ServletContext context = this.getServletContext();
  }
}

3. 域对象的功能

ServletContext是javaWeb四大域对象之一:

  1. PageContext;
  2. ServletRequest;
  3. HttpSession;
  4. ServletContext。

所有域对象都有存储数据的功能,因为域对象内部有一个Map,用来存储数据,下面是ServletConext用来操作数据的方法:

-void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:servletContext.setAttribute("xxx", XXX),在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。注:若多次调用该方法且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同。

-Object getAttribute(String name):用来获取ServletContext中的数据,但被获取的数据需被存储过才行。

-void removeAttribute(String name):用来移除ServletContext中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做;

-Enumeration getAttributeNames():获取所有域属性的名称;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容