[原]Spring学习笔记(十) --- 浅析Spring MVC的工作机制

祝一迪 17/11/01 21:52:41

本系列博客为spring In Action 这本书的学习笔记

终于学完了Spring的核心基础, 要开始学习web中的Spring了!!!
Spring MVC(Model-View-Controller)是Spring的一个web框架, 在正式开始学习Spring MVC的Controller之前, 我们先来简单了解一下Spring MVC的工作机制.


一. 跟踪Spring MVC的请求

要想了解Spring MVC, 我们可以通过一个请求从客户端发起, 经过Spring MVC的各组件, 最终又返回客户端的过程为契机, 来看一下Spring MVC的各组件之间是如何相互协作的.

每当用户在浏览器里面点击链接或者提交表单的时候, 请求就开始工作了, 一个请求会经过很多站点并在站点留下或带走一些信息, 最终又返回客户端. 下图展示了请求使用Spring MVC所经历的所有站点:
这里写图片描述

接下来就上面这张图, 我们来进行详细的分析.

1. DispatcherServlet

请求经过的第一站就是DispatcherServlet (1). 与大多数基于Java的web框架一样, Spring MVC的所有请求都会首先经过一个前端控制器Servlet(front controller servlet). 在一个web应用程序中通常会有多个控制器. 控制器就是Spring MVC中的Controller组件, 它主要用于处理各种请求. DispatcherServlet(前端控制器)与这些普通控制器的区别和关系是:

  • DispatcherServlet是单实例的, 也就是说在一个web应用程序中只会有一个DispatcherServlet. 而普通控制器在一个应用程序中会有多个.
  • DispatcherServlet将请求委托给应用程序的普通控制器来对请求进行实质的处理.

2. 处理器映射(Handler mapping)

前面提到了请求的第一站是DispatcherServlet, DispatcherServlet负责将请求分发给普通控制器. 但是在此之前, DispatcherServlet如何确定要将请求委托给哪个控制器呢? 这就需要用到处理器映射(2)(Handler mapping).

  • DispatcherServlet通过查询一个或多个处理器映射来确定应该将请求发送给哪个控制器.

3. 控制器(Handler)

一旦通过处理器映射选择了合适的控制器, DispatcherServlet会将请求发送给选中的控制器(3)(又称处理器(Hadler), 实质上就是Controller组件).

到达了控制器的请求会卸下其负载(比如用户提交的信息)并等待控制器处理这些信息. (实际上设计良好的控制器本身只处理很少的甚至不处理工作, 而是将业务逻辑委托给一个或者多个服务对象进行处理.)

控制器在完成逻辑处理之后, 通常会产生一些信息, 这些信息需要返回给用户并在浏览器上显示. 这些信息被称为模型(model). 这些信息需要一用户友好的方式进行格式化, 所以信息需要发送给一个视图(view) , 通常是JSP.

4. 模型及逻辑视图名(Model And View)

控制器所做的最后一件事就是将模型数据打包, 并且标示出用于渲染输出的视图名, 也就是请求这一站需要经过模型及逻辑视图名 (4), 然后控制器将模型和视图名(ModelAndView)一同送回DispatcherServlet.

这样做的好处就是控制器不会与特定的视图像耦合. 有过web项目经验的小伙伴都知道哦呵, 之前我们在控制器中处理完数据后, 通常会使用重定向或请求转发将请求转发给一个JSP, 而这个JSP通常用xxx.jsp这样的格式来指定.
而对于Spring MVC而言, 返回给DispatcherServlet的视图名并不直接表示某个特定的JSP, 或者它想表示的根本就不是一个JSP, 它仅仅是传递了一个逻辑名称, 这个逻辑名称将会通过视图解析器(ViewResolver)来查找真正的结果视图.

5. 视图解析器(View Resolver)

在上面的描述中, 我们清楚了DispatcherServlet将会通过视图解析器(5)来为逻辑视图名匹配一个特定的视图实现.

6. 视图(View)

既然DispatcherServlet已经确定了由哪个视图 (6)渲染结果, 那么请求只需要将模型数据交付给视图渲染, 请求的任务就完成了.

7. 响应(Response)

视图将使用模型数据渲染输出, 这个输出将会通过响应对象传递给客户端 (7).


通过上面跟踪一个Sping MVC的请求, 我们大致了解到了Spring的内部是按照一个什么样的流程进行工作的. 那么下面, 我们就来看一下Spring MVC的总体设计和M-V-C分别是怎样的设计.

二. Spring MVC的总体设计

现在我们已经知道Spring MVC中最重要的就是DispatcherServlet. 那么先来看一下DispatcherServlet类的继承关系, 再对它进行简单分析:
这里写图片描述

1. DispatcherServlet类的初始化方法initStrategies()

DispatcherServlet类继承了HttpServlet, 在Servlet的init方法调用时DispatcherServlet执行Spring MVC的初始化工作. 而具体的DispatcherServlet的初始化是在initStrategies()方法中完成的, DispatcherServlet初始化了什么, 都可以在initStrategies()中知道, 了解initStrategies()方法里面都干了些什么将会对后面学习Sping MVC的组件有很大的帮助, 我们会在后面的学习中不断地用到这个方法里面的东西. 这个方法的源代码如下:

程序1: DispatcherServlet类的initStrategies()方法的源代码:

    protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);
        this.initLocaleResolver(context);
        this.initThemeResolver(context);
        this.initHandlerMappings(context);
        this.initHandlerAdapters(context);
        this.initHandlerExceptionResolvers(context);
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);
        this.initFlashMapManager(context);
    }

initStrategies()里面分别干了8件事, 分别如下所述:

  • 1. initMultipartResolver: 初始化MultipartResolver, 用于处理文件上传服务, 如果有文件上传, 那么会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest, 并且将每个上传的内容封装成CommonsMultipartFile对象.
  • 2. initLocaleResolver: 用于处理应用的国际化问题, 通过解析请求的Locale和设置响应的Locale来控制应用中的字符编码问题.
  • 3. initThemeResolver: 用于定义一个主题, 例如, 可以根据用户的喜好来设置用户访问的页面的样式, 可以将这个样式作为一个Theme Name保存, 保存在用于请求的Cookie中或保存在服务端的Session中, 以后每次请求根据这个Theme Name返回特定的内容.
  • 4. initHandlerMappings: 用于定义用户设置的请求映射关系. HandlerMapping把用于用户请求的URL映射成一个个Handler实例.
  • 5. initHandlerAdapters: 用于根据Handler的类型定义不同的处理规则. 在handlerMapping中将URL映射成一个Controller实例, 那么Spring MVC在解析handlerAdapter对象时就会调用这个Controller实例.
  • 6. initHandlerExceptionResovler: 当Handler处理出错时, 会通过这个Handler来统一处理, 默认的实现类是SimpleMappingExceptionResolver, 将错误日志记录在log文件中, 并传到默认的错误页面.
  • 7. initRequestToViewNameTranslator: 将指定的ViewName按照定义的RequestToViewNameTranslator替换成想要的格式, 如加上前缀或者后缀等.
  • 8. initViewResolvers: 用于将View解析成页面, 在ViewResolvers中可以设置多个解析策略, 比如按照JSP解析, 或按照别的模板解析. 默认的解析策略是InternalResourceViewResolver, 按照JSP来解析.

从上面的初始化策略中可以看出, 在一个请求中可能需要我们我们来扩展的地方都定义了扩展点, 只要实现相应的接口类, 并创建一个Spring Bean就能扩展Spring MVC框架.
下面是Spring MVC的组件图:
这里写图片描述

在Spring MVC框架中, 其中的3个组件是用户必须要定义和扩展的: 定义URL映射规则, 实现业务逻辑的Handler实例对象(Controller), 渲染模板资源(View). 而连接Handler实例对象和模板渲染的纽带就是Model模型了.

下面再看看DispatcherServlet启动时都做了哪些事情?

2. DispatcherServlet启动时都干了哪些事?

Spring容器的创建是在FrameworkServlet的initServletBean()方法中完成的, 这个方法会创建WebApplicationContext对象, 并调用其refresh()方法来完成配置文件的加载, 配置文件的加载是先查找Servlet的init-param参数中设置的路径, 如果没有, 会根据namespace+Servlet的名称来查找XML文件. Spring容器在加载时回调用DispatcherServlet的initStrategies()方法来完成DispatcherServlet中定义的初始化工作, 也就是初始化Spring MVC的框架需要的8个组件, 这8个组件对应的8个Bean对象都保存在DispatcherServlet类中.


下面我们来看一下M-V-C的设计, 并分别将这3部分对应到Spring MVC的8个核心组件中.

三. Controller设计

1. HandlerMapping和HandlerAdapters组件

Spring MVC的Controller主要是由HandlerMapping和HandlerAdapters两个组件提供.
HandlerMapping负责映射用户的URL和对应的处理类, HandlerMapping并没有规定这个URL与应用的处理类如何映射, 在HandlerMapping接口中只定义了根据一个URL必须返回一个由HandlerExecutionChain代表的处理链, 我们可以在这个处理链中添加任意的HandlerAdapters实例来处理这个URL对应的请求.

1. HandlerMapping初始化

(1) 定义一个HandlerMapping

HandlerMapping就是我们上面说的处理器映射, DispatcherServlet通过它将URL和Handler(控制器Controller)对应起来.

Spring MVC本身提供了很多HandlerMapping的实现, 默认使用的BeanNameUrlHandlerMapping, 可以根据Bean的name属性映射到URL中. 比如我们定义这样一个Bean:

    <bean id="demo" name="/demo" class="WebXu.Demo">
        <property name="viewPage" value="/demo.html" />
    </bean>
    <!--通过alias标签来为Bean提供多个名称, 而这些所有的名称都指向同一个Bean. -->
    <!--为Bean增加别名, 时为了让应用的每一个组件能更容易的对公共组件进行引用.-->
    <alias name="/demo" alias="/demo1" />
    <alias name="/demo" alias="/demo2" />

如果没有定义其它的HandlerMapping, Spring MVC框架则自动将/demo.html映射到WebXu.Demo这个处理类, 所有以/demo.html为alias的Bean都会被映射到这个URL中. 在Bean的定义中也支持简单的正则匹配的方式, 如/demo*会匹配所有以/demo为前缀的URL.

Spring MVC提供的几种HandlerMapping实现类基本上都是基于配置的实现方式, 也就是URL的所有匹配规则都需要我们在配置文件中进行定义.

总的来说, HandlerMapping的作用就是帮助我们管理URL和处理类的映射关系, 简单理解就是将一个或多个URL映射到一个或多个Spring Bean中.

下面我们以SimpleUrlHandlerMapping为HandlerMapping的实现类, 在仔细看一看Spring MVC是如何将URL映射到我们定义的Bean的.

(2) SimpleUrlHandlerMapping是如何将URL映射到Bean上的?

我们先来看一下HandlerMapping类和SimpleUrlHandlerMaping类的相关类继承结构图:
这里写图片描述

可以看到HandlerMapping提供了一个实现了Ordered接口并继承了WebApplicationObjectSupport类的抽象类AbstractHandlerMapping, 在这个类中可以通过设置setOrder()方法提高优先级, 并且通过覆盖initApplicationContext()的方法实现初始化的一些工作. 那么到了SimpleUrlHandlerMapping类, 它的初始化过程如下:

1. 执行ApplicationObjectSupport类的setApplicationContext()方法, 再执行initApplicationContext()方法;
2. 接着执行SimpleUrlHandlerMapping类自己的方法initInterceptors()方法. 这个方法就是将SimpleUrlHandlerMapping中定义的(用户自定义的)interceptors包装成HandlerInterceptor对象保存在adaptedInterceptors数组中. 这一步的操作就是前面提到的HandlerMapping根据URL返回的那个由HandlerExecutionChain代表的处理链里面的操作. handlerExecutionChain对象中将会包含用户自定义的多个handlerInterceptor对象;
3. 接着是执行SimpleUrlHandlerMapping类的registerHandlers()方法, 将SimpleUrlHandlerMapping中定义的mappings注册到handlerMap集合中;
4. 最后就是调用getBean()方法.


HandlerMapping的初始化工作中最重要的两个工作就是将URL与Handler的对应关系保存在HandlerMap集合中, 并将所有的interceptor对象保存在adaptedInterceptors数组中, 等到请求来临执行所有的interceptor对象.所有的interceptor对象都必须实现HandlerInterceptor接口.

2. HandlerAdapter初始化

HandlerMapping可以完成URL和Handler的映射关系, 那么HandlerADapter就可以帮助自定义各种Handler了.

Spring MVC首先帮助我们把特别的URL对应到一个Handler接口, 但是这个Handler接口类不是固定的, 也就是我们的URL对应的Handler接口可以实现多个接口, 每个接口可以定义不同的方法.

Spring MVC中提供了下面三个典型的简单HandlerAdapter实现类:

  • SimpleServletHandlerAdapter: 可以继承HttpRequestHandle接口, 多有的Handler可以实现其void handlerRequest(HttpServletRequest req, HttpServletResponse resp, Object handler)方法;
  • SimpleControllerHandlerAdapter: 可以继承Controller接口, 所有的Handler可以实现其public ModelAndView handle(HttpServletRequest req, HttpServletResponse resp Object handler)方法, 该方法会返回ModelAndView对象, 用于后续模板的渲染;
  • SimpleServletHandlerAdapter: 可以直接继承Servlet接口, 可以将一个Servlet作为一个Handler来处理请求.

Spring MVC的HandlerAdapter机制可以让Handler的实现变得更加灵活, 不需要和其它框架一样只能和某个Handler接口绑定起来.
HandlerAdapter的初始化只是简单创建一个HandlerAdapter对象, 将这个HandlerAdapter对象保存在DisptcherServlet的handlerAdapters集合中. 当Spring MVC将某个URL对应到某个Handler时, 在handlerAdapters集合中找寻supports这个Handler的handlerAdapter对象, 然后将handlerAdapter对象返回, 并调用这个handlerAdapter接口对应的方法. 如果这个handlerAdapter对象是SimpleControllerHandlerAdapter, 则调用Controller接口的public ModelAndView handler(req, resp, handler)方法.

如果用户没有自定义HandlerAdapter的实现类, Spring MVC框架将提供默认的4个HandlerAdapter实现类:

  • HttpRequestHandlerAdapter
  • SinpleServletHandlerAdapter
  • SimpleControllerHandlerAdapter
  • SimpleServletHandlerAdapter.

2. Controller的调用逻辑

Controller的处理逻辑的关键就是DispatcherServlet的handlerMpapings集合中根据请求的URL匹配每一个handlerMapping对象中的handler(private final Object handler), 匹配成功之后将会返回这个handler的处理, 并连接handlerExecutionChain对象. 而这个handlerExecutionChain对象中将会包含用户自定义的多个handlerInterceptor对象.
HandlerExecutionChain类的getHandler()方法是Object类型的, 所以说这里返回的Handler对象是没有类型的. Handler的类型是由HandlerAdapter决定的. DispatcherServlet会根据Handler对象在其handlerAdapter集合中匹配哪个HandlerAdapter实例来支持该Handler对象, 接下来就执行Handler对象的相应方法. 如该Handler对象的相应方法返回一个ModelAndView对象, 接下来就去执行View渲染.

Controller的大致调用流程如下:
1. 整个Spring MVC的调用是从DispatcherServlet的doervice()方法开始的, 在doService()方法中会将ApplicationContext/localeResolver/themeResolver等对象添加到request中以便于后面使用.
2. 接着就是调用doDispatch()方法, 这个方法就用来处理用户请求.
3. 调用checkMutipart()方法(mutipart是一种数据格式), 这个方法会检查当前的请求是否为post请求, contentType是否以mutipart/为前缀, 如果是, 将request包装成MutipartHttpRequest对象.
4. 执行getHandler()方法.
5. new HandlerExecutionChain(): 在handlerMappings集合中依次匹配每个HandlerMapping的getHandler(request)方法直到某个HandlerMapping的URL匹配成功, 并返回HandlerExecutionChain对象.
6. getIntercepters():获取该Handler中定义的所有Intercepter对象.
7. 执行HandlerIntercepter的preHandler()方法, 如果返回false, 当前请求在执行afterHandler()之后将立即返回.
8. 执行getHandlerAdapter()方法.
9. 得到一个HandlerAdapter对象ha, 调用ha.supports()方法, 该方法会返回在HandlerAdapters集合中第一个支持该Handler的HandlerAdapter对象.

四. Model设计

分析完Controller组件的设计, 接下来来看Model组件的设计. 如果你有写过一丢丢Spring MVC框架的web程序, 那么你就大概能理解, DI(依赖注入)在Spring MVC的一部分就是通过Model体现的.

在Controller的HandlerAdapter组件的初始化中提到HandlerAdapter接口的其中一个简单实现类:SimpleControllerHandlerAdapter, 它的handle方法会返回一个ModelAndView对象, ModelAndView对象是连接业务逻辑层与View展现层的桥梁, 或者说就是连接Handler(Controller)与View的桥梁.

ModelAndView对象就是持有一个ModelMap对象和一个View对象(或者View的名称). ModelMap对象就是执行模板渲染时所需要的变量对应的实例. 比如我们要在JSP中使用req.getAtribute(String)获取的属性是一个对象, 那么这个对象就和它的名称一起存在ModelMap中.**
ModelMap也是一个Map, 在Handler中将模板中需要的对象存在这个Map中, 然后传递到View对应的ViewResolvers中, 不同的ViewResolvers会对这个Map中的对象由不同的处理方式.比如, JSP中将每一个ModelMap中的元素分别设置到request.setAttribute(modelName,modelValue)中.

五. View设计

1. RequestToViewNameTranslator和ViewResolver组件

对于Spring MVC的View模块来说, 它由两个组件支持: RequestToViewNameTranslator和ViewResolver.

  • RequestToViewNameTranslator主要支持用户自定义对ViewName的解析. 比如为了得到视图的名字, 会使用前缀”/WEB-INF/Views/”和后缀”.jsp”进行拼接.
  • ViewResolver主要是根据用户请求的ViewName创建合适的模板引擎来渲染最终的页面, ViewResolver会根据ViewName创建一个View对象, 调用View对象的void render(Map model, HttpServletRequest req, HttpServletResponse resp)方法渲染页面. 还有, ViewResolver接口有一个抽象的实现类AbstarctCachingViewResolver, 这个类定义了一个抽象方法View loadView(String viewName, Locale locale), 根据viewName创建View对象.

2. View的调用逻辑

View的大致调用流程如下:
1. 最先调用DispatcherServlet类的getDefaultViewName()方法, 如果Handler中返回的ModelAndView对象的ViewName没有设置, 那么就会调用viewNameTranslator获取的ViewName.
2. 调用RequestToViewNameTranslator的getViewName()方法.
3. 调用LocaleResolver接口的resolverLocale()方法.
4. 调用ViewResolver接口的resolverViewName方法, 返回View对象, 然后调用createView()方法, 将ViewClass属性对应的InternalResolverView实例化.
5. 最后调用InternalResolverView的render()方法渲染出JSP页面.

作者:dela_ 发表于 2017/11/01 21:52:41 原文链接 https://blog.csdn.net/dela_/article/details/78419219
阅读:327 评论:1 查看评论