Servlet到底是什么玩意儿

634 阅读10分钟

1. Servlet介绍

(1) Servlet是什么?

狭义的说,Servlet是JavaEE的一种技术规范。
广义的说,运行在服务器端的实现了该规范的java程序,都叫做Servlet。

(2) 服务器不是Tomcat吗,跟Servlet有啥关系?

Servlet只是Java程序,它需要部署到服务器上才能发挥作用。
而事实上,服务器可以分为很多种,比如应用服务器、Web服务器、数据库服务器等等,而Tomcat只是Web服务器中的一种,它被称为轻量级的Web服务器。
因此,Tomcat既是Web服务器,也是Servlet容器。

(3) 为啥Tomcat被称为轻量级的Web服务器,它到底轻在哪了呢?

原来,Sun公司在创建Java语言的时候,推出了3个版本:JavaSE、JavaEE、JavaME。

  • JavaSE是标准版,用来开发部署在桌面、服务器等环境中使用的Java应用程序。比如电脑上的应用软件。
  • JavaME是微型版,用来开发移动设备、机顶盒以及嵌入式等环境中的Java程序。(跟Android可不是一个东西)
  • JavaEE是企业版,是在JavaSE的基础上构建的,用来帮助我们开发和部署可移植、健壮、可伸缩且安全的服务器端Java应用程序。它提供Web服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构。比如用来做服务应用、网站开发等。

在JavaEE中,包含一大堆技术规范,而Tomcat主要只是实现了其中的Servlet和JSP规范,所以,就很轻。

(4) 服务器为什么需要Servlet?

服务器自己不能实现功能吗,要Servlet干啥?

我们先来看下客户端在访问服务器时,Tomcat中发生了些什么:

  1. 服务器启动创建线程池,并监听TCP端口。
  2. 客户端创建TCP连接发送HTTP请求(其实就是一段文本),服务器从线程池中取出一个线程来处理发送过来的请求,按照HTTP协议的格式来进行解析。解析的结果封装为一个HttpServletRequest对象,同时,线程会创建一个HttpServletResponse对象,用来收集业务逻辑处理之后的响应信息。
  3. 解析之后,线程根据请求路径,去Servlet容器中找到对应配置的Servlet对象,这个过程就是路由。
  4. Servlet对象调用其service方法,将request和response作为入参,然后进入业务逻辑。
  5. 业务逻辑处理完之后,线程将response中的数据按照HTTP协议的格式生成HTTP响应的文本。
  6. 线程通过TCP连接发送该响应文本,然后断掉连接,并回到线程池中。

可以看到,除了业务逻辑之外,其他的比如接收请求和响应请求等都是共性功能。程序员其实只关心自己的业务逻辑,所以这些共性功能就由服务器来实现。虽然这些共性功能的处理逻辑是不同的,但是它们都遵循了Servlet规范。

所以,Servlet其实就是服务器按照规范,实现共性功能的那部分程序。它一般由服务器厂商提供接口,然后由由程序员自己继承其接口进行差异化实现,或者可以直接使用某些框架内的实现。

(5) Servlet和MVC中的Controller、Service、Dao是啥关系?

Controller、Service、Dao其实已经是业务逻辑的实现层了,Servlet在它们的前边进行请求的解析,又在后边进行响应的处理。
所以说,Servlet是客户端与服务器或应用程序之间的中间层

(6) Servlet的生命周期是什么?

其实根据上边的介绍,这个问题的答案已经呼之欲出了:

1) 加载和实例化

Servlet对象的加载其实有两种情况,一种是在服务启动时进行加载,一种是请求到来时进行加载,它取决于Servlet的配置load-on-startup。

load-on-startup的值表示该Servlet的优先级,如果值是非负数,越小优先级越高,在启动时就越优先加载这个Servlet。如果值是负数或者没有设置,则请求到来时才进行加载。

2) 初始化

加载完成后,Servlet容器会创建Servlet实例并调用init方法进行初始化。init方法只会被调用一次,它用来创建或加载一些数据,这些数据将被用于Servlet的整个生命周期。

3) 处理请求

service方法时执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调service方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

每次服务器接收到一个Servlet请求时,服务器会产生一个新的线程并调用服务。service方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。

4) 终止

当Web应用被终止,或者Servlet容器终止运行,或者Servlet容器重新装载Servlet新实例时,Servlet容器会调用Servlet的destroy方法进行销毁。destory方法也只会被调用一次,在调用之后,该servlet对象被标记为垃圾,等待回收。

其实整个生命周期的过程,看看Tomcat中Servlet接口也就大概清楚了: image.png

2. Servlet实现

介绍完了这么多,那到底应该怎么去实现一个Servlet呢?
其实很简单,既然规范都已经有了,直接去创建一个实现了Servlet接口的类,并实现其中的方法就行了。

就拿Tomcat来举例,一般可以直接继承HttpServlet抽象类,它实现了Servlet接口。

public class MyServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
        // 设置响应内容类型
        response.setContentType("text/html");

        // 实际的逻辑是在这里
        PrintWriter out = response.getWriter();
        out.println("<h1>Hello World!</h1>");
    }
}

上边就实现了一个简单的Servlet,编译后部署在Tomcat目录下的 /webapps/ROOT/WEB-INF/classes 中,并在Tomcat目录下的 /webapps/ROOT/WEB-INF/web.xml 文件中新增以下内容:

<web-app>      
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>MyServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/MyServlet</url-pattern>
    </servlet-mapping>
</web-app>  

最后启动Tomcat,在浏览器上访问http://localhost:8080/MyServlet 就可以看到Hello World!的页面。

不对吧,Servlet不是要实现init、service、destory等等方法吗?这里怎么实现了个doGet方法?

其实在HttpServlet及其父类GenericServlet类中就能找到答案:原来这些方法已经实现好了,只不过有的被实现为空了。
主要可以关注下具体执行逻辑的service方法:

protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        // 省略
        doGet(req, resp);
        // 省略
    } else if (method.equals(METHOD_HEAD)) {
        // 省略
        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 {
        // 省略
    }
}

原来如此,这里用了模板方法模式,开发者只需要去实现doGet、doPost等等方法就行了。
在service方法的注释上也说的很清楚:There's no need to override this method.

我们上边只实现了doGet方法,那如果来的是Post请求,会怎么样呢?
点开doPost方法的默认实现,可以看到其中的sendMethodNotAllowed方法会在response中写入错误信息:

private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
    String protocol = req.getProtocol();
    // Note: Tomcat reports "" for HTTP/0.9 although some implementations
    //       may report HTTP/0.9
    if (protocol.length() == 0 || protocol.endsWith("0.9") || protocol.endsWith("1.0")) {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    }
}

这下明白了,原来我们平时经常见到的400、404、405之类的错误码是这么回事。

3. SprintBoot中的Servlet

使用过SpringBoot的同学可能会有疑问:我咋没实现过什么Servlet,难道不是直接就开始写Controller的代码吗?

那有没有想过这样一种可能:Servlet已经给你实现好了。

(1) DispatcherServlet

事实上,这应该属于SpringMVC的功劳,它所实现的DispatcherServlet类是SpringMVC统一的入口,所有的请求都通过它。

DispatcherServlet是前端控制器,配置在web.xml中,Servlet依自已定义的具体规则拦截匹配的请求,分发到目标Controller来处理。

详细的DispatcherServlet分析可以看这篇文章,这里我做一个大概的流程总结:

1) Tomcat启动

  • DispatcherServlet同样也是继承自HttpServlet,它的init方法在其父类HttpServletBean中实现,主要作用是加载web.xml中DispatcherServlet的配置。

  • 加载配置之后,调用initServletBean方法建立webApplicationContext上下文,将SpringMVC配置文件中定义的Bean加载到上下文中,并将上下文添加到ServletContext中。

  • 建立好上下文后,通过onRefresh方法初始化SpringMVC。初始化会把Spring容器和SpringMVC容器中所有的HandlerMapping实例和HandlerAdapter实例放入列表中,这些实例是在初始化webApplicationContext上下文时创建的。

2) 客户端发送请求

  • Tomcat收到客户端的请求之后,如果匹配到DispatcherServlet在web.xml中配置的映射路径,就会将请求转交给DispatcherServlet处理。

  • 列表中的每个HandlerMapping实例都会根据请求信息,去寻找处理该请求的Handler(执行程序,比如Controller中的方法)。

  • DispatcherServlet会从列表中找到可以处理该Handler的HandlerAdapter实例来进行处理,得到ModelAndView对象。

3) 服务端响应请求

  • 根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet。ViewResolver结合Model和View来渲染视图,最后将渲染结果返回给客户端。

(2) ServletContainerInitializer

看完上边的介绍后的同学可能还有个疑问:虽然有了DispatcherServlet,但是我也确实没配置过web.xml文件啊?

我们得弄清楚,为什么需要web.xml,因为它是用来配置Servlet的。如果我们配置好了DispatcherServlet并创建了上下文,那就不需要再配置web.xml了。

于是,在Servlet 3.0规范中,出现了一个新接口:ServletContainerInitializer,它的作用是能够通过编程的方式,动态的来注册Servlet、Filter、Listener的功能。

1) Tomcat中的ServletContainerInitializer

打开TomcatStarter类,可以看到它也是实现了ServletContainerInitializer,并实现了其中的onStartUp方法。

@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
   try {
      for (ServletContextInitializer initializer : this.initializers) {
         initializer.onStartup(servletContext);
      }
   }
   catch (Exception ex) {
      this.startUpException = ex;
      // Prevent Tomcat from logging and re-throwing when we know we can
      // deal with it in the main thread, but log for information here.
      if (logger.isErrorEnabled()) {
         logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
               + ex.getMessage());
      }
   }
}

通过调试可以看到,this.initializers中就有ServletWebServerApplicationContext,通过调用其中的selfInitialize方法,从而实现了对DispatcherServlet和上下文的配置。

2) SpringMVC中的ServletContainerInitializer

在spring-web包的META-INF/services下,有一个javax.servlet.ServletContainerInitializer文件,里面只有一行:

org.springframework.web.SpringServletContainerInitializer

原来是使用了SPI的方式,加载了SpringServletContainerInitializer类。
这个类的代码如下:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

   @Override
   public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
         throws ServletException {

      List<WebApplicationInitializer> initializers = Collections.emptyList();

      if (webAppInitializerClasses != null) {
         initializers = new ArrayList<>(webAppInitializerClasses.size());
         for (Class<?> waiClass : webAppInitializerClasses) {
            // Be defensive: Some servlet containers provide us with invalid classes,
            // no matter what @HandlesTypes says...
            if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                  WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
               try {
                  initializers.add((WebApplicationInitializer)
                        ReflectionUtils.accessibleConstructor(waiClass).newInstance());
               }
               catch (Throwable ex) {
                  throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
               }
            }
         }
      }

      if (initializers.isEmpty()) {
         servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
         return;
      }

      servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
      AnnotationAwareOrderComparator.sort(initializers);
      for (WebApplicationInitializer initializer : initializers) {
         initializer.onStartup(servletContext);
      }
   }

}

@HandlesTypes(WebApplicationInitializer.class)注解会将所有WebApplicationInitializer接口的子类和接口传到onStartup方法的形参webAppInitializerClasses上,将不是接口和抽象类的类进行实例化,并添加到集合中。最后遍历调用集合中每一个对象的onStartup方法。

因此,如果我们想自定义初始化器,动态添加Servlet、Filter和Listener,也可以直接实现WebApplicationInitializer接口,交给Spring初始化就好了。
更详细的内容可以参考这篇文章

(3) 自定义实现Servlet

除了上边介绍的方式,还有两种更为简洁常用的注册Servlet的方法。

1) @WebServlet注解实现

@WebServlet("/hello")
public class MyServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
        // 设置响应内容类型
        response.setContentType("text/html");

        // 实际的逻辑是在这里
        PrintWriter out = response.getWriter();
        out.println("<h1>Hello World!</h1>");
    }
}

在Servlet类上使用了@WebServlet注解,这样访问http://localhost:8080/MyServlet 也可以看到Hello World!
当然,别忘了在启动类上增加@ServletComponentScan注解,这样才能扫描到我们创建的Servlet。

2) ServletRegistrationBean实现

@Configuration
public class ServletAutoConfiguration {
    @Bean
    public ServletRegistrationBean<HttpServlet> myServlet() {
        return new ServletRegistrationBean<>(new MyServlet(), "/hello");
    }
}

ServletRegistrationBean继承自RegistrationBean,后者是SpringBoot中广泛应用的注册类。
除了注册Servelt,它还可以用来注册Filter (FilterRegistrationBean) 和ServletListener(ServletListenerRegistrationBean)。

参考引用

Tomcat外传:zhuanlan.zhihu.com/p/54121733
为什么java需要servlet:www.zhihu.com/question/49…
web.xml中load-on-startup的作用:www.cnblogs.com/shamo89/p/9…
Servlet生命周期:www.runoob.com/servlet/ser…
Servlet实例: www.runoob.com/servlet/ser…
DispatcherServlet源码分析:www.jianshu.com/p/9b7883c6a…
使用SpringBoot之后web.xml去哪儿了:blog.csdn.net/weixin_3412…
SpringBoot的底层入口SpringServletContainerInitializer:blog.csdn.net/weixin_4373…
SpringMVC源码深度解析之SpringServletContainerInitializer原理分析:blog.csdn.net/chuanyingca…