美文网首页
Servlet简介与源码分析

Servlet简介与源码分析

作者: LENN123 | 来源:发表于2020-04-16 20:53 被阅读0次

前言

Servlet(即Server applet服务端小程序)是一个会在服务端被调用的Java程序,来处理请求。前面在分析Tomcat的时候我们知道,Tomcat本身包含了一个Servlet容器,用来存放Servlet实例,当有请求到来时就会调用对应的Servlet实例来处理。Servlet其实就是一个Java类,但如果它仅仅只是一个普通的Java类,Tomcat又如何知道该调用它的哪些方法呢?所以,我们设计了一个协议,或者说约定。当一个普通的Java类实现了某些约定的方法,就可以被看作是一个Servlet实例。因此Servlet更像是一座联系服务器和服务端程序的桥梁

Servlet的作用

Servlet 接口

Servlet接口里就给我们定义了一个合格Servlet类要实现哪些接口。

  • Servlet Interface
package javax.servlet;

import java.io.IOException;

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() 当一个Servlet类实例化的时候会被调用的逻辑
service() 对一个到来请求的具体处理逻辑
destory() 一个Servlet类实例被销毁时(通常是在tomcat关闭时)调用的逻辑

我们可以看到service()方法,Tomcat会把一个请求(注意这里请求并不一定是http请求,Servlet可以处理多种请求)包装成ServletRequest类的实例传入,service()处理完,我们再把处理结果包装成ServletResponse类的实例返回出去。理论上只要实现了这些接口,我们就可以构造出一个Servlet了,但我们却很少这么做,因为这个过程过于繁琐,JavaEE已经帮我们包装好了一些实现了Servlet接口的类,我们使用的时候只要简单的重写这些类中的某个方法就可以了。继承这些类然后重写一些方法就好了,比如对于处理http请求的Servlet,就提供了HttpServlet类供我们继承,我们来看看这些类的继承关系

类继承关系
上图我们可以看到,HttpServlet并没有直接实现Servlet接口,而是继承了GenericServlet类并在它的基础上针对http进行了定制化。我们看看GenericServlet做了什么。
  • GenericeServlet
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    private static final long serialVersionUID = 1L;
    private transient ServletConfig config;

    public GenericServlet() {
    }

    public void destroy() {
    }

    public String getInitParameter(String name) {
        return this.getServletConfig().getInitParameter(name);
    }

    public Enumeration<String> getInitParameterNames() {
        return this.getServletConfig().getInitParameterNames();
    }

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

    public ServletContext getServletContext() {
        return this.getServletConfig().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 message) {
        this.getServletContext().log(this.getServletName() + ": " + message);
    }

    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() {
        return this.config.getServletName();
    }
}

我们看到GenericServlet类只是简单的实现了3个借口中的方法,并没有什么实质性的代码,那么GenericServlet这个抽象类的意义是什么?其实它更多的是提供一个模版,供子类去修改,而不是让每个子类都去实现这些接口中的所有方法,避免重复劳动。下面让我们具体分析继承了GenericServletHttpServlet做了什么。

  • HttpServlet类实现
    HttpServlet

HttpServlet依然是一个抽象类,提供了一个模版。我们可以看到其内部重载定义了2Service方法,我们先看最基础的以ServletRequest为参数的service()方法。

  • public void service(ServletRequest req, ServletResponse res)
    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(lStrings.getString("http.non_http"));
        }

        this.service(request, response);
    }

逻辑很简单,就是把请求和响应都强制类型转换成HttpServletRequestHttpServletResponse类型,再调用protected void service(HttpServletRequest req, HttpServletResponse resp)方法。若强制类型转换失败则说明该请求不是一个http请求,直接抛出异常即可。

  • protected void service(HttpServletRequest req, HttpServletResponse resp)
    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;
                try {
                    ifModifiedSince = req.getDateHeader("If-Modified-Since");
                } catch (IllegalArgumentException var9) {
                    ifModifiedSince = -1L;
                }

                if (ifModifiedSince < lastModified / 1000L * 1000L) {
                    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()方法很长,但是逻辑很简单,就是根据http请求中的方法去调用对应该方法的处理逻辑,比如http请求是GET方法就调用doGet(),若是POST就调用doPost()。再让我们看看假设http请求是GET方法,在doGet()中会发生什么。
-protected void doGet(HttpServletRequest req, HttpServletResponse resp)

    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(405, msg);
        } else {
            resp.sendError(400, msg);
        }
    }

它直接告诉我们这个方法还未实现,然后返回错误参数。所以doGet()doPost()...等方法才是我们真正要实现的方法,来完成对应请求的处理逻辑。(init()destory()也需要们实现)

配置和使用Servlet

下面利用利用实现一个简单的web项目,来实践Servlet,首先用IDEA创建一个默认的web项目

  • 项目结构


    项目结构
  • src下编写一个Servlet实例HelloServlet

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("HelloServlet中doGet()方法被调用了!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}
  • index.jsp里设置一个超链接来指向这个Servlet
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
      <a href="HelloServlet">点击调用HelloServlet</a><br/>
  </body>
</html>

让我们来运行这个项目, 然后点击这个超链接,但却给我们报了一个404错误。我们之前说了tomcat内部有一个Servlet容器。就像配置Spring IOC容器一样,我们需要通过配置的方式告诉tomcat我们这个web应用里有哪些Servlet,还要告诉tomcat当访问哪些路径时,调用这个Servlet实例。配置以上信息的方式有2种,web.xml和注解的方式,我们先尝试使用xml的方式。

  • 修改web.xml配置servlet
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/HelloServlet</url-pattern>
    </servlet-mapping>

这里有2部分组成, <servlet></servlet>表示注册一个Servlet实例到tomcatservlet容器中,类的名字叫HelloServlet,并给他取了一个别名叫hello。第二部分 <servlet-mapping></servlet-mapping>则说明了一个url和对应servlet实例的映射关系。表示当访问localhost:8080/webstudy/HelloServlet路径时,会去调用一个别名为helloServlet实例对象的相应方法。配置好后让我们实验一下。

Connected to server
[2020-04-16 09:47:09,326] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Deploy took 275 milliseconds
HelloServlet中doGet()方法被调用了!

因为超链接是一个GET方法,所以这里HelloServletdeGet()方法被调用了,配置生效。

  • 利用注解的方式配置Servlet
    Servlet3.0之后开始支持注解的的方式配置Servlet,编写一个WelcomeServlet类,并用注解的方式把它配置到tomcat中去。
@WebServlet("/WelcomeServlet")
public class WelcomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("WelcomeServlet的doGet()方法被调用了");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

@WebServlet("/WelcomeServlet")表示当访问localhost:8080/webstudy/WelcomeServlet时,会调用这个对应的Servlet中的对应方法。让我们添加一个链接到index.jsp中去来测试该配置。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>$Title$</title>
</head>
<body>
    <a href="HelloServlet">点击调用HelloServlet</a><br/>
    <a href="WelcomeServlet">点击调用WelcomeServlet</a><br/>
</body>
</html>

运行,并点击点击调用WelcomeServlet这个链接,后台打印信息如下。

WelcomeServlet的doGet()方法被调用了

说明该配置方法也是生效的。

Servlet生命周期

我们之前提到过Servlet提供了3个主要方法,HttpServlet重点重写了service()方法,而init()destroy()方法并没有重写,GenericHttp中也只是对这2个方法给出了空实现。我们知道,service()方法会在每个请求到来的时候被调用,来处理请求。那么这init()destroy()个方法会在什么时候调用呢,让我们来实验一下。

  • 修改HelloServlet
public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("HelloServlet的init()方法被调用了");
    }

    @Override
    public void destroy() {
        System.out.println("HelloServlet的destory()方法被调用了");
    }
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("HelloServlet中doGet()方法被调用了!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

部署运行该项目, 观察控制台的输出

Connected to server
[2020-04-16 11:22:40,748] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Deploy took 225 milliseconds
16-Apr-2020 23:22:50.448 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory 把web 应用程序部署到目录 [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager]
16-Apr-2020 23:22:50.478 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager] has finished in [30] ms
HelloServlet的init()方法被调用了
HelloServlet中doGet()方法被调用了!

我们发现当tomcat启动的时候,init()方法并没有被调用,也就说HelloServlet并没有在tomcatservlet容器中生成一个实例对象,而是直到第一次调用HelloServlet中的doGet()方法时,这个Servlet才被实例化并加入tomcat容器中去,此时init()方法才被调用。是一种懒加载的思想。那么有没有什么办法,让tomcat能够在启动的时候就把这些Servlet实例化然后加载到容器中去呢?我们可以在web.xml添加如下标签。

  • 修改web.xmlHelloServlettomcat启动时就加载到容器中去
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/HelloServlet</url-pattern>
    </servlet-mapping>

<load-on-startup>1</load-on-startup>表示在tomcat启动时就实例化并加载到tomcatservlet容器里,中间的数字可以指明多个Servlet的加载顺序,现在让我们重新启动tomcat

  • 启动时init()就被调用
Connected to server
[2020-04-16 11:38:31,671] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
[2020-04-16 11:38:31,913] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:38:31,914] Artifact webstudy:war exploded: Deploy took 243 milliseconds

ServletConfig类

我们一直重写的init()方法都是无参数的,和Servlet接口中定义的同名方法并不一样,我们来看看Servlet接口中是如何定义的。

  • Servlet接口中定义的init()方法
    void init(ServletConfig var1) throws ServletException;

我们可以看到这里有一个ServletConfig类型的形参。我们一直重写的无参init()方法实际上由GenericServlet提供。

  • GenericServlet中的init()
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }
    public void init() throws ServletException {
    }

GenericServlet自动的帮我们把传入的ServletConfig保存进一个类变量里,然后再调用我们重写的无参init()的方法。显然这个ServletConfig类型的变量是由tomcat生成并传入进来的,如同它的名字一样,代表了一个Servlet的配置。那么如何使用这个类呢。再看看GenericServlet中的其他方法。

  • GenericServlet
    public ServletConfig getServletConfig() {
        return this.config;
    }
    public String getInitParameter(String name) {
        return this.getServletConfig().getInitParameter(name);
    }

发现我们可以利用getInitParameter(String name)来获得一个Servlet的初始参数配置,而这些配置则是以键值对的形式存在的,我们可以在web.xml定义这些和Servlet配置有关的键值对。

  • web.xml定义InitParameter
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
        <init-param>
            <param-name>servletParam</param-name>
            <param-value>servletValue</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

在些init-param的作用范围是一个Servlet内部,调用这个Servlet的所有请求都可以拿到这些init-param,让我们做个实验。

  • 修改HelloServlet获取init-param
public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("HelloServlet的init()方法被调用了");
        System.out.println(super.getInitParameter("servletParam"));
    }
}

运行tomcat可以在控制台看到如下信息

Connected to server
[2020-04-16 11:46:10,610] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
servletValue

我们成功获得了键servletParam对应的值servletValue
除了在web.xml配置一个Servletinit-param,我们还可以用注解的方式定义。

  • 利用注解定义init-param
@WebServlet(value = "/WelcomeServlet", initParams = {@WebInitParam(name = "servletParam", value = "servletValue")})
public class WelcomeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("WelcomeServlet的doGet()方法被调用了");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

ServletContext

ServletConfig代表了一个Servlet的配置信息,其中主要包含了很多初始化参数。ServletContextServletConfig相似,只不过它的作用范围是整个tomcatServlet容器,也就是说所有的Servlet实例都可以访问到ServletConfig中的信息。GenericServlet提供了一个方法可以让我们拿到这个类的实例

   public ServletContext getServletContext() {
        return this.getServletConfig().getServletContext();
    }

可以看到ServletContexttomcat包装进每一个ServletServletConfig中,再在初始化的时候传入,从而可以共享这些信息。我们可以在web.xml中定义这些信息。

  • 定义context-param
    <context-param>
        <param-name>globalParam</param-name>
        <param-value>globalValue</param-value>
    </context-param>

  • 获取这些context-param
public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("HelloServlet的init()方法被调用了");
        System.out.println(super.getInitParameter("servletParam"));
        ServletContext servletContext = super.getServletContext();
        System.out.println(servletContext.getInitParameter("globalParam"));
    }
}

启动tomcat,观察控制台

Connected to server
[2020-04-17 02:21:40,825] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
servletValue
globalValue

成功获得context-param

相关文章

网友评论

      本文标题:Servlet简介与源码分析

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