深入剖析Spring Web源码(九) - 处理器映射,处理器适配器以及处理器的实现 - 基于注解控制器流程的实现

 

4.2.1.2 基于注解控制器流程的实现

 

上一节,我们详细的分析了基于简单控制器流程的实现,事实上,许多的简单控制器的实现已经不被推荐使用。自从 2.5发布以后, Spring开始鼓励使用基于注解控制器的流程。基于注解的控制器流程具有实现方法简单,程序代码清晰易读等特点。

 

基于注解控制器的流程和基于简单控制器的流程的实现非常相似,派遣器 Servlet在处理一个 HTTP请求的时候,它通过缺省注解处理器映射 (DefaultAnnotationHandlerMapping) HTTP请求映射到响应的注解控制器 (@Contoller),然后,把控制流传给注解方法处理器适配器 (AnnotationMethodHandlerAdapter)。注解方法处理器适配器并不是简单的传递控制流给注解控制器,而是以一定规则查找注解控制器里面的处理器方法,并且通过反射的方式映射 HTTP 请求信息到方法参数,然后使用反射调用方法,得到方法的返回结果后,再根据一定的规则把返回结果映射到模型和视图对象,进而返回给作为总控制器的派遣器 Servlet

 

事实上,缺省注解处理器映射的实现重用了简单控制器流程的处理器映射的实现体系结构。回顾上一小结中分析的 Bean URL处理器映射继承自抽象探测 URL处理器映射,实现了其抽象方法 determineUrlsForHandler(),在这个方法实现中,把所有以左划线 (/)开头的 Bean名字注册作为一个简单的控制器。缺省注解处理器映射的实现同样也实现了抽象方法 determineUrlsForHandler(),如果某个 Bean中使用了请求映射 (@RequestMappings)注解,则注册这个 Bean作为一个注解控制器。如下类图所示,

 

 

图表 4 ‑20

 

如上图所示,缺省注解处理器映射实现了方法 determineUrlsForHandler(),查找 Bean类型级别的请求映射注解和方法级别的请求映射注解,如果两个级别的请求映射注解都存在,结合两个级别的请求映射注解,否则使用方法级别的请求映射注解,构造出一个 URL Pattern集合,并且返回这个 URL Pattern集合,抽象探测 URL处理器映射就会使用这个集合的每一个元素作为关键字注册当前的 Bean作为一个注解控制器。如下程序注释,

 

 

在缺省注解处理器映射中除了实现了提取注解处理器中配置的 URL Pattern 外,还改写了一个校验处理器的方法 validateHandler() ,这个方法的实现根据类型级别的请求映射的配置,校验当前的请求是否能够应用到这个处理器上。这个校验方法是在派遣器 Servlet 将一个请求映射到响应的处理器的时候调用的。具体逻辑如下,

  • 如果处理器类型级别请求映射定义了 HTTP 方法,则当前 HTTP 请求方法必须是请求映射定义的这些 HTTP 方法之一。
  • 如果处理器类型级别请求映射定义了 HTTP 参数,则当前 HTTP 请求参数必须包含请求映射定义的所有的 HTTP 参数。
  • 如果处理器类型级别请求映射定义了 HTTP 头,则 HTTP 请求头必须包含请求映射定义的所有的 HTTP 头。

 

否则,这个处理器不能处理当前的 HTTP 请求,抛出异常,终止处理。可见,类型级别的请求映射是优先校验的,方法级别的请求映射是后来校验的,所以,我们得出一下结论。

  •   方法级别请求映射定义的 HTTP 方法必须是类型级别请求映射定义的 HTTP 方法的子集,否则没有意义。
  • 方法级别请求映射定义的 HTTP 参数可以多余类型级别请求映射定义的 HTTP 参数。
  • 方法级别请求映射定义的 HTTP 头可以多余类型级别请求映射定义的 HTTP 头。

 

如下程序注释,

 



 

 

通过以上我们的分析,我们理解在基于注解控制器流程的实现中,缺省注解处理器映射是通过处理器中声明的请求映射注解注册注解控制器以及查找注解控制器的。作为总控制器的派遣器 Servlet 通过 HTTP 请求得到一个注解控制器,将注解控制器等传给注解方法处理器适配器进行处理器方法的调用,对处理器方法的调用是通过反射实现的,在调用之前,需要通过反射从请求参数,请求头等探测所有需要的参数,再调用后返回方法结果后,再通过一定规则映射结果到模型和视图对象。这个流程是在注解方法处理器适配器类和相关的支持类实现的,如下类图所示,

 

图表 4 ‑21

 

如上图所示,注解方法处理器适配器类实现了处理器适配器接口。在 handle() 方法的实现中,使用两个辅助类 Servlet 处理器方法解析器 (ServletHandlerMethodResolver) Servlet 处理器方法调用器 (ServletHandlerMethodInvoker) 利用反射的原理调用注解处理器中的处理器方法。在处理器方法调用之前,通过参数注解从 HTTP 请求中提取参数值,在处理器方法调用之后通过注解映射方法的返回值到模型和视图对象,最后返回给派遣器 Servlet 进行视图解析和视图显示。如下流程图所示,

 

 

图表 4 ‑22

 

根据上面流程图显示的顺序,我们将深入的对代码进行剖析,派遣器 Servlet 从缺省注解处理器映射得到了注解控制器后,将控制器传递给注解方法处理器适配器的 handle() 方法, handle() 方法在进行通用的 HTTP 请求方法检查和设置 HTTP 响应缓存信息后,根据需要对处理器方法进行同步或者非同步的调用,如下代码注释,

 

 

 

 

如上代码注释,对于如何解析处理器方法,如何解析参数,如何调用处理器方法以及如果映射返回值等的实现都是封装在处理器方法解析器和处理器方法调用器的实现中的,我们稍后会深入剖析这些逻辑的实现。

 

注解方法处理器适配器也对处理器适配器接口的另外两个方法进行实现,如下代码注释,

 

 

  

 

如上程序注释可见, supports() getLastModified() 的实现是非常简单的,这里不再详细分析。通过上面的流程图和代码注释,我们也已经大体的了解了通过反射调用处理器方法的总体步骤,现在我们开始深入剖析注解方法处理器适配器是如何实现方法解析,方法调用以及模型结果数据映射的。

 

如何解析处理器方法呢?

正如注解控制器的名字所示,它是基于注解信息的控制器,这个控制器是一个普通的 Bean, 不需要实现任何接口或者继承抽象类。一个注解控制器可能包含一个或者更多的处理器方法,这些处理器方法是用请求映射注解 (@RequestMapping) 标志的,请求映射注解包含着用于匹配 HTTP 请求的 URI Pattern, 请求方法,请求参数,请求头的信息。这些在请求映射中声明的信息会用于匹配 HTTP 请求,如果匹配成功,则会使用匹配的处理器方法处理请求。我们首先分析请求映射注解 (@RequestMapping) 都包含哪些属性信息。

 

 

 

 

如上代码所示,声明在一个处理器方法或者处理器类型级别的请求映射注解可以包含 URI Pattern, 请求 方法,请求 参数, 请求头等信息。在匹配的时候, URI Pattern 是最重要的匹配信息,如果没有指定 URI Pattern ,则使用其余信息匹配。下面我们分析注解方法处理器适配器是如何使用这些信息匹配一个 HTTP 请求到一个处理器方法的。

 

首先,注解方法处理器适配器为每一个处理器类型创建一个处理器方法解析器,处理器方法解析器通过反射分析处理器类型,并且取得所有声明了请求映射注解的处理器方法,初始化绑定方法,模型属性方法。如下代码所示,

 

 

 

 

我们看到,对于一个处理器类型初始化一个处理器方法解析器,处理器方法解析器在解析处理器方法时使用了一个复杂的逻辑决定这些方法中的哪些方法可以处理当前 HTTP 请求。如果多个处理器方法可以处理当前的请求,那么选择最佳匹配的处理器方法。以下详细分析这个工作流程。

 

在初始化阶段,我们已经存储了所有声明了请求映射注解的处理器方法。现在我们遍历所有的处理器方法,判断是否此方法支持当前的 HTTP 请求。

 

如果请求映射注解的处理器方法包含 URI Pattern 的信息,那么对于每一个 URI Pattern ,则查看是否存在类型级别的 URI Pattern ,如果存在,则结合类型级别的 URI Pattern 。否则,如果最佳匹配的 URI Pattern 存在,则结合最佳匹配的 URI Pattern 。否则单独使用处理器方法级别的 URI Pattern 匹配当前的查找路径。如果 URI Pattern 匹配成功,查看是否 HTTP 请求匹配声明的请求方法,请求参数和请求头。如果这些信息都匹配成功,则添加当前 URI Pattern 到匹配路径集合中,并通过路径匹配对比器对匹配路径集合进行排序,同事标识当前处理器方法为匹配。

 

如果请求映射注解的处理器方法不包含 URI Pattern 的信息,则只需要查看是否匹配声明的请求方法,请求参数和请求头。如果这些信息匹配,则认为当前处理器方法为匹配。一种特殊情况是,如果请求映射注解中没有声明 HTTP 方法和参数,那么首先使用处理器方法名解析器解析处理器方法名,如果解析的方法名和当前处理器方法相同,则认为当前处理器方法为匹配。缺省的方法名解析器是通过去掉 URI 最后一部分的文件名扩展名得到的。

 

如果某一个处理器方法匹配,存储这个处理器方法到匹配的处理器方法集合中。如果处理器集合中已经存在一个处理器方法,而且已存处理器方法和现在处理器方法不是同一个处理器方法,那么我们需要解析冲突。在这种情况下,如果没有路径信息,我们需要使用方法名解析器解析最佳处理器方法。处理规则如下,

 

1.        如果已存处理器方法和当前处理器方法名相同,则使用后解析的方法。

2.        否则,如果解析的方法名和已存处理器方法同名,继续使用已存处理器方法。如果解析的方法名和当前处理器方法同名,则使用当前处理器方法。

3.        如果解析的方法名既不等于当前处理器方法也不等于已存的处理器方法,则抛出异常,终止处理。

 

根据上面的逻辑分析,最终如果有一个或者多个处理器方法匹配当前 HTTP 请求,则通过请求映射信息对比器找到最佳匹配的处理器方法。如下代码所示,

 

 

  

 

如何解析处理器方法参数呢?

 

注解方法处理器适配器是通过请求映射注解解析处理器方法的。解析得到了处理器方法,在调用处理器方法之前我们必须首先解析所有的处理器方法参数。处理器方法参数也是通过各种注解标记的,不同的注解包含着信息指导注解方法处理器适配器从不同的数据源取得数据。下面我们详细分析,处理器方法参数所支持的所有注解。

 

最常用的应用在处理器方法参数上的注解是请求参数注解 (@RequestParam) 。 请求参数注解指导注解方法处理器适配器通过参数名字找到请求参数值,并且赋值给当前方法参数。如下代码注释,

 

 

  

 

请求头注解 (@RequestHeader) 指导注解方法处理器适配器通过头名字找到请求头的值,并且赋值给当前方法参数。如下代码注释,

 

 

 

 

请求体注解 (@RequestBody) 指导注解方法处理器适配器通过消息转换器将请求体转换成 Java 对象作为当前方法参数的值。但是请求注解并没有声明任何属性信息,它只是个标志,指导注解方法处理器适配器为当前参数解析请求体。如下代码注释,

 

 

 

 

Cookie 值注解 (@CookieValue) 指导注解方法处理器适配器通过 Cookie 名字找到 Cookie 的值,并且赋值给当前方法参数。如下代码注释,

 

 

 

 

路径变量注解 (@PathVariable) 指导注解方法处理器适配器通过路径变量名字找到路径变量的值 ( 路径变量通常被称为模板变量 ) ,并且赋值给当前方法参数。如下代码注释,

 

 

 

 

模型属性注解 (@ModelAttribute) 指导注解方法处理器适配器通过模型属性名字找到模型属性的值,并且赋值给当前方法参数。模型属性注解也可以用于声明在方法上,这种情况下把方法的返回值作为模型数据值放入隐式模型中。如下代码注释,

 

 

 

 

对于一个处理器方法参数,只能声明上面的注解中的一个,或者不声明注解。如果声明了上面注解中的一个,则根据注解解析处理器方法参数的值。如果一个处理器方法参数没有声明任何注解,则查看是否配置有客户化 Web 参数解析器,如果存在则使用客户化 Web 参数解析器进行解析。如果没有配置客户化 Web 参数解析器或者客户化 Web 参数解析器不能解析当前参数,则判断是不是标准的 WebRequest 类型,如果是则返回标准的 WebRequest 类型。如果上面的情况仍然不能解析参数值,则使用默认值。

 

如果通过上面的流程仍然没有解析出参数值,则做如下判断,

 

·          如果参数是模型类型或者 Map 类型,则使用当前隐式模型。

·          如果参数是 Session 状态类型,则使用当前的 Session 状态,它表明当前 Session 完成或者没有完成。

·          如果参数是 HTTP 实体,则解析 HTTP 请求体作为 HTTP 实体。

·          如果是错误对象,则必须前一个参数是绑定对象。否则,抛出异常,终止处理。下面将介绍绑定对象。

·          如果是简单数据类型,则初始化参数名为空。

 

最后,如果参数值仍然没有被解析,那么这个参数是一个需要绑定的 Bean 。如果这个方法参数声明了模型属性或者这个模型属性是 Session 属性,则从模型中或者 Session 中取得当前值,否则根据类型创建一个对象,再使用 HTTP 请求参数进行绑定操作。

 

此外,值注解 (@Value) 和校验注解 (@Valid) 是两个辅助的注解,并不直接绑定方法参数到任何 HTTP 请求信息。用来声明方法参数缺省值或者校验的。

 

值注解 (@Value) 用于给参数值指定缺省值,如果没有指定任何注解或者指定的注解所表达的数据为空,而且注解中没有指定缺省值,则使用这个指定的缺省值。如下代码注释,

 

 

 

 

校验注解 (@Valid) 指导注解方法处理器适配器对当前的属性进行校验。这个注解没有任何属性,仅仅是一个标志。

 

  

 

 

整个流程实现在处理器方法解析器类中。如下代码所示,

 

 

 

 

我们理解,对于不同的参数注解,注解方法处理器适配器从不同的数据源提取数据,例如,请求参数,请求头或者请求体等,下面我们具体分析对于不同的参数注解,注解方法处理器适配器是如何解析方法参数值的。

 

下面是通过 HTTP 方法参数解析处理器方法参数值的 (@RequestParam) 。如下代码注释,

 

 

 

 

解析 HTTP 请求头作为处理器方法参数值的实现和解析 HTTP 请求参数作为处理器方法参数值的实现相似 (@RequestHeader) 。这里不再进行代码注释。

 

下面是通过 HTTP 请求体解析处理器方法参数值的 (@RequestBody) 。如下代码注释,

 

 

  

 

事实上,有另外一个方法可以用来解析请求体作为处理器方法参数,那就是通过声明请求参数作为 HttpEntity 类型, HttpEntity 类型可以指定模板类型进行泛化。对比请求体注解, HttpEntity 参数不需要注解,注解方法处理器适配器是通过类型来判断是否需要解析方法体的。如下代码注释,

 

 

 

 

我们知道,请求体注解 (@RequestBody) HTTP 实体 (HttpEntity) 的实现都是通过消息转换器进行解析 HTTP 请求体作为处理器方法参数的。消息转换器的设计和处理器适配器十分相似,消息转换器有一个方法判断是否一个消息转换器可以转换某个类型的参数,当然,还有另外一个方法用于事实上转换请求体到一个指定类型的对象。后面我们将有一小节讨论 HTTP 消息转换器的实现体系结构。

 

下面是通过 Cookie 值解析处理器方法参数值的 (@CookieValue) 。如下代码注释,

 

 

 

 

下面是通过模板变量解析处理器方法参数值的 (@PathVariant) 。如下代码注释,

 

 

  

 

最后我们分析一下注解方法处理器适配器是如何绑定领域模型对象,初始化领域模型对象,和管理领域模型对象的。

 

当一个参数上没有声明任何注解,而且参数没有缺省值并且不是一个简单的属性,也就是说,它是一个领域对象模型对象,也就是一个 Bean 对象。那么则需要数据绑定对象把请求参数数据绑定到 Bean 的属性上。请看解析参数主流程的代码,这个片段的代码如下注释所示,

 

 

  

 

首先,它在隐式模型中查找领域对象模型 Bean ,因为这个领域对象模型可能是被标志有模型属性注解 (@ModelAttribute) 的方法返回的。如果隐式模型中不包含领域对象模型 Bean ,而且当前领域对象模型 Bean 是一个 Session 属性,则在 Session 中取得领域对象模型 Bean 。如果仍然不能解析领域对象模型 Bean ,则通过反射实例化一个全新的类型进行绑定。如下代码注释,

 

 

  

 

现在我们看到,一个绑定对象初始化完毕后,这包括应用在所有处理器上的绑定初始化器的初始化,和声明在处理器内部的绑定初始化方法,然后开始做真正的绑定操作。这个操作就是把 HTTP 请求参数设置到绑定的目标对象中。如下代码所示,

 

 

  

 

Web 数据绑定是数据绑定的子类,主要是分析 HTTP 请求参数包含的数据,这包括多部文件 HTTP 请求,然后,将收集的参数信息赋值给绑定的目标对象。如下代码注释,

 

 

  

 

数据绑定,属性校验,属性编辑器的实现是一个复杂的话题,属于 Spring 框架的核心实现,我们不在这里进行分析和代码注释。

 

如何调用处理器方法呢?

 

在得到方法参数以前,注解方法处理器适配器首先处理模型属性方法,初始化隐式模型,如果存在 Session 属性,导入 Session 属性,然后使用解析得到的方法参数调用处理器方法。如下代码所示,

 

 

  

 

 

如果映射处理器方法返回值和隐式模型到模型和视图对象呢?

 

注解方法处理器适配器调用一个处理器方法之后,得到了处理器方法的返回值,它根据返回值的类型解析模型和视图对象,如果声明了消息响应体,它将使用消息转换器转换返回值到 HTTP 响应体。我们已经分析了消息转换器的实现体系结构,这里不再进行分析。如下代码所示,

 

 

  

 

如何更新模型数据呢?

 

最后,如果存在 Session 属性,则导出 Session 属性从模型到 Session 中,如下代码所示,

 

 

  

 

基于简单控制器流程的实现和基于注解控制器流程的实现是两个用来处理 HTTP 请求的 Spring Web MVC 流程的实现。他们使用了不同的方式实现了相同的功能和流程,简单控制器流程是经典的实现,而基于注解控制器流程的实现是从版本 2.5 新引入的,其简单易用又不失于功能强大,使基于注解控制器流程的实现成为被推荐使用的流程。

 

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值