《看透Spring MVC 源代码分析与实践》读书笔记——详解Servlet

346 阅读7分钟

  最近在看《看透Spring MVC 源代码分析与实践》这本书,觉得真心不错。这本书不单单讲的是Spring MVC,第一章为网站基础知识,包括网站架构、常见协议、Servlet和Tomcat等内容。虽然在做开发的时候并不会直接用到,不过理解了之后可以让我们在进行具体开发的时候更加得心应手,还可以通过对具体内容的学习学到一些优秀思想。这次我想先从最简单的Servlet内容谈起,毕竟每个Java Web开发人员应该对Servlet都不陌生。文章内容主要是对书中知识点的归纳总结,外加一部分自己的理解。
  大家都知道Servlet是Server+Applet的缩写,表示一个服务器应用。其实Servlet就是一套规范,按照这套规范写的代码就可以直接在Java的服务器上运行。Servlet 3.1中Servlet的结构如下图所示(来源:www.54tianzhisheng.cn/2017/07/09/… ,大家可以没事看看这个博客,有些内容讲的很好):

图片未加载成功
  Servlet 3.1中Servlet接口的定义如下:

public interface Servlet {
  
    public void init(ServletConfig config) throws ServletException;
  
    public ServletConfig getServletConfig();
  
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
  
    public String getServletInfo();
  
    public void destroy();
}

  init方法在容器启动时被容器调用且只会调用一次(当load-on-startup设置为负数或不设置时会在Servlet第一次被用到时被调用)。init方法被调用时,由容器传给它一个ServletConfig类型的参数,ServletConfig顾名思义指的是Servlet的配置,我们在web.xml中定义Servlet时通过init-param标签配置的参数就是通过ServletConfig来保存的;
  getServletConfig方法用于获取这个ServletConfig对象;
  service方法用于具体处理一个请求,它处理的请求是不限请求类型的。具体怎么处理呢,一会讲到实现它的方法的时候会说;
  getServlet方法可以获取一些Servlet相关的信息,需要自己实现,默认返回空字符串;
  destroy方法主要用于在Servlet销毁(一般指关闭服务器)时释放一些资源,也只会调用一次。

ServletConfig

  ServletConfig接口定义如下:

public interface ServletConfig {
  
    public String getServletName();
  
    public ServletContext getServletContext();
  
    public String getInitParameter(String name);
  
    public Enumeration<String> getInitParameterNames();
}

  getServletName用于获取Servlet的名字,也就是我们在web.xml中定义的servlet-name;
  getInintParameter用于获取init-param配置的参数;
  getInitParameterNames用于获取配置的所有init-param的名字集合;
  getServletContext的返回值ServletContext代表这个应用本身。 我身边很多人都不明白“代表应用本身”是什么意思,这是说ServletContext里设置的参数,可以被当前应用的所有Servlet共享。大家经常在Session或Application中保存参数,而后者很多时候就是保存在了ServletContext中。可以把ServletConfig理解成Servlet级的,而ServletContext是Context(也就是Application)级的。当然,ServletContext的功能要强大很多,并不是保存一下配置参数。
  可能上面说的比较抽象,下面来举一个例子。在web.xml文件中写如下配置信息:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>application-context.xml</param-value>
</context-param>
<servlet>
    <servlet-name>DemoServlet</servlet-name>
    <servlet-class>com.hawk.DemoServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>demo-servlet.xml</param-value>
    </init-param>
</servlet>

  上面通过context配置的contextConfigLocation配置到了ServletContext中,而通过servlet下的init-param配置的contextConfigLocation配置到了ServletConfig中。在Servlet中可以分别通过它们的getInitParameter方法进行获取:

String contextLocation = getServletConfig().getServletContext().getInitParameter("contextConfigLocation");
String servletLocation = getServletConfig().getInitParameter("contextConfigLocation");

  为了操作方便,GenericServlet定义了getInitParameter方法,内部返回getServletConfig().getInitParameter的返回值,因此如果需要获取SevletConfig中的参数,可以直接调用getInitParameter,而不必再调用getServletConfig()。
  ServletContext中常见的用法就是保存Application级的属性,通过调用setAttribute方法:

getServletContext().setAttribute("contextConfigLocation", "new path");

  而ServletConfig不可设置属性。

GenericServlet

  GenericServlet是Servlet的默认实现(注意:GenericServlet是一个抽象类,service方法并未被实现),是与具体协议无关的Servlet,主要做了三件事:(1)实现ServletConfig接口,可以直接调用ServletConfig里的方法;(2)提供了无参的init方法;(3)提供了log方法。下面分别来解释一下:
  GenericServlet实现了ServletConfig接口,在需要调用ServletConfig中方法的时候可以直接调用,而不必先获取ServletConfig。比如,获取ServletContext的时候可以直接调用getServletContext,而无需调用getServletConfig().getServletContext(),不过和刚才说的getInitParameter一样,其底层实现其实是在内部调用了:

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

  GenericServlet实现了Servlet的init(ServletConfig config)方法,在里面将config设置给了内部属性config,然后调用了无参的init方法(这个无参的init方法是GenericServlet新增的,专门用于被它的子类覆盖):

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

  这么写的作用很明显:首先将参数config设置给了内部属性config,这样就可以在ServletConfig的接口方法中直接调用config的相应方法来执行;其次,之后我们在写Servlet的时候就可以只处理自己的初始化逻辑(即只需覆盖无参的init方法),而不需要再关心config,也不需要再调用super.init(config)了。一开始说到容器启动时会调用init(ServletConfig config)方法,而该方法会调用无参的init方法,从而实现我们自己的初始化逻辑。需要注意的是,如果在自己的Servlet中重写了带参数的init方法,一定要记着调用init(config),否则这里的config属性接收不到值,相应的ServletConfig接口方法就不能执行了。
  GenericServlet提供两个log方法,一个记录日志,一个记录异常,具体实现是通过传给ServletContext的日志实现的。一般我们都有自己的日志处理方式,所以这两个log方法用得不是很多。

HttpServlet

  HttpServlet是用HTTP协议实现的Servlet的子类,一般我们在写Servlet的时候就是直接继承这个类,Spring MVC中最重要的DispatcherServlet也是继承的这个类。分析这个类主要关心的是如何处理请求,HttpServlet主要重写了service方法,首先将ServletRequest和ServletResponse转换成HttpServletRequest和HttpServletResponse,然后根据Http请求的类型不同将请求路由到了不同的处理方法,代码如下:

    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request;
        HttpServletResponse response;
        // 转换request和reponse的类型
        // 如果请求类型不相符,则抛出异常
        try {
            request = (HttpServletRequest)req;
            response = (HttpServletResponse)res;
        } catch (ClassCastException var6) {
            throw new ServletException("non-HTTP request or response");
        }

        this.service(request, response);
    }

    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);
        }

    }    

  而像doGet、doPost这样的具体处理方法相信大家都很熟悉了,这里就不再概述,重点是HttpServlet将不同的请求方式路由到不同的处理方法这个实现思想。
  最后呢,想谈一下我个人对Servlet这种底层事物的看法。有的人在学了各种框架后,就对Servlet、JDBC这种底层事物很不屑,毕竟实际开发中Servlet是用不到的。但我觉得,无论多么高级的框架,本质上都是对这些底层事物的封装,区别在于封装的程度不同,了解这些底层事物的实现细节,对于框架的学习能起到事半功倍的作用。
  最后的最后,墙裂推荐《看透Spring MVC 源代码分析与实践》这本书,我觉得很OK。