在我的对于Tomcat容器介绍的文章中,介绍了Tomcat容器的工作原理,咱们晓得Tomcat容器在收到申请之后,会把申请解决为Request/Response对象,交给Servlet实例解决。对于Spring的Web利用,失去Tomcat容器的申请之后会交给DispatcherServlet去解决。DispatcherServlet是Spring Web利用解决申请的外围组件,本文会介绍DispatcherServlet的工作原理及要害源码,本文次要参考了Spring的官网文档。

<!--more-->

Servlet简介

什么是Servlet容器

首先咱们须要晓得什么是Web服务器,Web 服务器应用 HTTP 协定传输数据。在个别状况下,用户在浏览器(客户端)中键入 URL(例如www.baidu.com/static.html ), 并获取要读取的网页。所以服务器所做的就是向客户机发送一个网页。信息的替换采纳指定申请和响应音讯的格局的 HTTP 协定。

正如咱们看到的,用户/客户端只能从服务器申请动态网页。如果用户心愿依据本人的输出浏览网页,那么这还不够好。Servlet 容器的根本思维是应用 Java 动静生成服务器端的网页。所以 Servlet 容器实质上是与 Servlet 交互的 Web 服务器的一部分。


Servlet容器的实现往往比较复杂,以典型的Tomcat容器为例,容器内蕴含连接器和Container两大组件,以及类加载器、服务组件、服务器组件等多种组件。Servlet容器会简单把申请打包为规范的Request/Response,而后交个Servlet实例进行解决,下图为Tomcat容器的结构图。

什么是Servlet?

Servelt容器会负责解决申请并把申请转为Request/Response对象,然而Servlet容器不会理论解决业务逻辑,而是交给Servlet解决。Servlet 是 javax.servlet 包中定义的接口。它申明了 Servlet 生命周期的三个根本办法:init()、service() 和 destroy()。它们由每个 Servlet Class(在 SDK 中定义或自定义)实现,并由服务器在特定机会调用。

  • init() 办法在 Servlet 生命周期的初始化阶段调用。它被传递一个实现 javax.servlet.ServletConfig 接口的对象,该接口容许 Servlet 从 Web 应用程序拜访初始化参数。
  • service() 办法在初始化后对每个申请进行调用。每个申请都在本人的独立线程中提供服务。Web容器为每个申请调用 Servlet 的 service() 办法。service() 办法确认申请的类型,并将其分派给适当的办法来解决该申请。
  • destroy() 办法在销毁 Servlet 对象时调用,用来开释所持有的资源。

从 Servlet 对象的生命周期中,咱们能够看到 Servlet 类是由类加载器动静加载到容器中的。每个申请都在本人的线程中,Servlet 对象能够同时服务多个线程(线程不平安的)。当它不再被应用时,会被 JVM 垃圾收集。像任何Java程序一样,Servlet 在 JVM 中运行。为了解决简单的 HTTP 申请,Servlet 容器呈现了。Servlet 容器负责 Servlet 的创立、执行和销毁。

Servlet申请解决

那么Servlet容器是如何解决一个Http申请的呢?在我的另外一篇对于Tomcat容器中介绍了Tomcat容器解决Http申请的具体流程,此处就简略介绍一下逻辑:

  1. Web服务器接管HTTP申请。
  2. Web服务器将申请转发到Servlet容器。
  3. 如果对应的Servlet不在容器中,那么将被动静检索并加载到容器的JVM中。
  4. 容器调用init()办法进行初始化(仅在第一次加载 Servlet 时调用一次)。
  5. 容器调用Servlet的service()办法来解决HTTP申请,即读取申请中的数据并构建响应。
  6. Web 服务器将动静生成的后果返回到浏览器/客户端。

javax包中对于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();}

DispatcherServlet简介

Spring容器注册DispatcherServlet

上文中咱们晓得Servlet次要用于解决Request/Response对象,Spring Web利用中用于解决申请和响应的Servlet实现就是DispatcherServlet。如果学习过Tomcat或者其它Servlet容器相干的常识,咱们应该晓得一个Web利用容器容许有多个Servlet实例,能够通过门路或者其它路由规定进行路由。SpringBoot中咱们能够通过如下形式向容器中注册一个DispatcherServlet,注册实现之后SpringBoot会在Servlet容器中生成对应的组件(如Tomcat的Wrapper容器)。

public class MyWebApplicationInitializer implements WebApplicationInitializer {    @Override    public void onStartup(ServletContext servletContext) {        // Load Spring web application configuration        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();        context.register(AppConfig.class);        // Create and register the DispatcherServlet        DispatcherServlet servlet = new DispatcherServlet(context);        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);        registration.setLoadOnStartup(1);        registration.addMapping("/app/*");    }}

DispatcherServlet解决申请的流程

通过后面的学习咱们晓得Servlet的次要作用就是解决Request,DispatcherServlet解决申请的流程如下所示:

  1. 把DispatcherServlet对应的WebApplicationContext通过Request.setAttribute和Request进行绑定,这样每个Reqeust就有本人对应的ApplicationContext。
  2. 把用于国际化的LocalResolve和Request进行绑定。如果程序不须要格式化,则能够疏忽这部分逻辑。
  3. 把设置主题的ThemeResolve和Request进行绑定。如果程序不须要主题设置,则能够疏忽这部分逻辑。
  4. 如果申请中蕴含multipart文件,并且容器中蕴含MultipartResolver,那么会应用这个Resolver把申请中的文件封装为MultipartHttpServletRequest。MultiPart Resolver是Spring MVC的另外一个性能,我会在后续具体介绍。
  5. 为这个申请查找适合的HandlerMapping,适合的HandlerMapping能够从申请中获取适合的处理器链(蕴含预处理、后处理和Controller等逻辑)。
  6. 如果须要返回View,对View进行渲染,如果不须要那么间接返回body。

WebApplicationContext还提供了对立解决异样的HandlerExceptionResolver,用于解决申请过程中的异样。异样能够有多种解决策略:如解决@ExceptionHandler注解的ResponseStatusExceptionResolver,将异样解决为对应界面的SimpleMappingExceptionResolver等。

DispatcherServlet反对一些和Spring相干的非凡参数,比方蕴含DispatcherServlet的容器类型等:

字段名称阐明信息
contextClass蕴含了这个Servlet的ConfigurableWebApplicationContext,默认状况下是XmlWebApplicationContext
contextConfigLocation用于指定上下文配置文件的地位,能够用逗号宰割指定多个文件
namespaceWebApplicationContext的命名空间
throwExceptionIfNoHandlerFound当存咋Handler找不到的状况时,是否抛出异样

DispatcherServlet蕴含的组件与配置

Web申请的解决流程比较复杂,DispatcherServlet会应用Spring容器中的一些非凡的Bean来帮忙解决申请。这些Bean有默认实现,然而用户也能够应用自定义实现来代替默认实现逻辑。DispatcherServlet蕴含的要害组件及各个组件之间的合作原理如下所示。

从下面的DispatcherServlet结构图能够看进去,DispatcherServlet解决申请的过程中须要多个组件协调工作,接下来咱们会一一介绍各个组件的性能及基本原理。

  1. Web配置:用于配置Servlet的属性,能够通过Bean或者文件的模式进行配置。
  2. 处理器映射器HandlerMapping:次要性能是依据申请获取对应的拦截器列表和解决申请的程序。
  3. 处理器适配器HandlerAdaptor:调用申请理论对应处理器的适配器,封装了理论调用处理器的逻辑。
  4. 理论处理器Controller:理论的业务逻辑都封装在这外面,由适配器反射调用。
  5. 各种Resolver:比方异样解决、视图解析和国际化解析等等。

DispatcherServlet组件配置

在下面的介绍中,咱们晓得DispatcherServlet会调用很多非凡组件来解决申请,DispatcherServlet会在ApplicationContext的Refresh阶段去容器中找对应的Bean,如果没有找到自定义的Bean组件,那么会应用默认的Bean组件,这些组件在DispatcherServlet.properties文件中有定义。

在大多数状况下咱们并不需要自定义组件,而仅仅须要批改默认组件的参数,比方增加类型转换服务和自定义校验逻辑等等,这种状况下最好的方法是配置WebMVC Config,对于MVC Config的配置会在我的另外一篇文章中进行介绍。

DispatcherServlet注册配置

咱们晓得在Tomcat容器中须要配置web.xml文件,在外面须要指定Servlet的类和Servlet的映射门路。在Spring中咱们也能够自定一个Servlet,并且指定Servlet解决的URL门路。咱们能够通过如下的形式向Spring Web容器中注册一个Servlet。

import org.springframework.web.WebApplicationInitializer;public class MyWebApplicationInitializer implements WebApplicationInitializer {    @Override    public void onStartup(ServletContext container) {        XmlWebApplicationContext appContext = new XmlWebApplicationContext();        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));        registration.setLoadOnStartup(1);        registration.addMapping("/");    }}

WebApplicationInitializer类是Spring提供的一个用于初始化Servlet容器的接口,Spring会通过ServiceLoader去程序中查找并加载WebApplicationInitializer并调用其onStartup办法。有时候咱们可能只须要向容器中注册一个Servlet,并不需要配置Servlet的其它参数,那么咱们能够通过继承Spring提供的形象实现这个性能,Spring针对注解和xml配置文件有两个抽象类,其应用办法如下所示:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {    @Override    protected Class<?>[] getRootConfigClasses() {        return null;    }    @Override    protected Class<?>[] getServletConfigClasses() {        return new Class<?>[] { MyWebConfig.class };    }    @Override    protected String[] getServletMappings() {        return new String[] { "/" };    }}
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {    @Override    protected WebApplicationContext createRootApplicationContext() {        return null;    }    @Override    protected WebApplicationContext createServletApplicationContext() {        XmlWebApplicationContext cxt = new XmlWebApplicationContext();        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");        return cxt;    }    @Override    protected String[] getServletMappings() {        return new String[] { "/" };    }}

如果咱们只须要对容器中曾经存在的Servlet增加Filter,那么咱们也只须要继承Spring提供的另外一个抽象类AbstractDispatcherServletInitializer,而后重写对应的办法。如果你须要依照本人的要求生成DispatcherServlet,你也能够重写createDispatcherServlet办法。

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {    // ...    @Override    protected Filter[] getServletFilters() {        return new Filter[] {            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };    }}

DispatcherServlet与Web利用

对于Tomcat容器和Springboot之间的集成形式,我在其它文章中有具体介绍,此处再简略说一下原理:Springboot在启动的时候会依据包中的类名判断容器的类型,是Web利用的状况下获取对于Web容器的配置,而后依据配置生成Tomcat容器。Web利用类型的Spring容器会蕴含ServletContext和Servlet配置相干的信息。常见的WebApplicationContext接口定义如下所示:

public interface WebApplicationContext extends ApplicationContext {    String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";    String SCOPE_REQUEST = "request";    String SCOPE_SESSION = "session";    String SCOPE_APPLICATION = "application";    String SERVLET_CONTEXT_BEAN_NAME = "servletContext";    String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";    String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";    @Nullable    ServletContext getServletContext();}

DispatcherServlet门路匹配

URL的划分

DispatcherServlet从Tomcat中获取的Request中蕴含了残缺的URL,并且会依照Servlet的映射门路把门路划分为contextPath、servletPath和pathInfo三局部,三者之间的关系如下所示。

DispatcherServlet在收到申请后,须要依据门路去查找对应的HandlerMapping,这个门路通常状况下是不蕴含Servlet容器映射到Servlet容器的门路,如ContextPath和局部ServletPath。如下图中的红色方框局部所示。

URL的编码

在因特网上传送URL,只能采纳ASCII字符集,也就是说URL只能应用英文字母、阿拉伯数字和某些标点符号,不能应用其余文字和符号,这意味着如果URL中有汉字,就必须编码后应用。国际标准并没有对编码格局进行标准,然而咱们罕用的浏览器会采纳“%”+UTF8的模式进行编码。如下的示例中显示了URL编码前和编码后的比照。

那么Spring在匹配对应的门路的时候应该应用编码前的门路还是编码后的门路呢?因为编码门路是浏览器或者框架的操作,用户并不知道这一部分逻辑,对于用户来说,始终应该只晓得解码后的门路,所以Spring的门路匹配始终应该应用解码后的门路。

门路匹配问题

servletPath和pathInfo蕴含的是解码之后的门路信息,解码之后的门路无奈再和原始的RequestURL进行门路匹配,这可能会带来一些问题:如果门路中蕴含编码解码的要害字符(如:“/”和“;”),会导致解码呈现问题。此外不同的Servlet容器可能应用不同的解码形式,这也可能带来一些匹配方面的问题。

Spring默认应用的servletPath是"/" ,这并不会带来门路匹配的问题,如果用户须要自定义servletPath,就须要对这方面多加关注了。

DispatcherServlet拦截器

Spring提供了HandlerInterceptor拦截器接口让用户对每次申请进行加工解决(如权限校验),所有类型的HandlerMapping都反对HandlerInterceptor。该接口一共蕴含三个办法:

  1. preHandle:在调用解决申请的Handler之前调用该办法,返回false示意该办法不非法。
  2. postHandle:在调用解决申请的Handler之后调用该办法。
  3. afterCompletion:申请解决实现之后调用该办法。
public interface HandlerInterceptor {    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)            throws Exception {        return true;    }    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,            @Nullable ModelAndView modelAndView) throws Exception {    }    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,            @Nullable Exception ex) throws Exception {    }}

DispatcherServlet异样解决

DispatcherServlet解决申请的过程中,如果呈现了异样,DispatcherServlet会去异样解决链中查找适合的HandlerExceptionResolver,并且由HandlerExceptionResolver生成对应的View。Spring提供了多种HandlerExceptionResolver,列表及性能如下:

HandlerExceptionResolver阐明
SimpleMappingExceptionResolver用于把异样类映射为对应的谬误界面
DefaultHandlerExceptionResolver把异样映射为对应的HttpCode
ResponseStatusExceptionResolver用于解决@ResponseStatus对应的HttpCode
ExceptionHandlerExceptionResolver解决@ExceptionHandler办法异样,能够参考此处

DispatcherServlet会一一调用HandlerExceptionResolver,直到其中一个异样处理器返回View或者调用完所有的异样处理器。

其它组件

下面的文章中,咱们次要介绍了DispatcherServlet的一些要害组件,还有一些视图组件、国际化组件和主题组件等此处只做简略介绍。

  1. 视图解析:视图解析组件次要用于将响应渲染为页面,对于Json格局的放回则不进行渲染;
  2. 国际化:如时区切换、申请头语言、Coooke和Session等都须要国际化组件的参数;
  3. 主题组件:用于切换网页的主题,应用的比拟少;
  4. Multipart:通常用于上传文件的解析,该组件会把“multipart/form-data”申请中的数据转为MultipartHttpServletRequest。
  5. 日志组件:比方是不是打印申请详情等。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

本文最先公布至微信公众号,版权所有,禁止转载!