关于java:SpringMVC-解析二DispatcherServlet

34次阅读

共计 9563 个字符,预计需要花费 24 分钟才能阅读完成。

在我的对于 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 用于指定上下文配置文件的地位,能够用逗号宰割指定多个文件
namespace WebApplicationContext 的命名空间
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

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

正文完
 0