您的位置:首页 > 编程语言 > Java开发

SpringMVC 解析(二)DispatcherServlet

2022-02-09 13:46 831 查看

在我的关于Tomcat容器介绍的文章中,介绍了Tomcat容器的工作原理,我们知道Tomcat容器在收到请求之后,会把请求处理为Request/Response对象,交给Servlet实例处理。对于Spring的Web应用,得到Tomcat容器的请求之后会交给DispatcherServlet去处理。DispatcherServlet是Spring Web应用处理请求的核心组件,本文会介绍DispatcherServlet的工作原理及关键源码,本文主要参考了Spring的官方文档

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

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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: