1 鸟瞰Spring MVC
Spring MVC(Model View Controller)框架的处理控制器的实现策略,与其他的请求驱动的Web框架在总体思路上是相似的。通过引入Front Controller和Page Controller的概念来分离流程控制逻辑与具体的Web请求处理逻辑。下面,我们简要介绍一下Spring MVC框架中的各个组件以及执行流程。
1.1 DispatcherServlet
org.springframework.web.servlet.DispatcherServlet
就是Spring MVC中的Front Controller,它负责接收并处理所有的Web请求,只不过针对具体的处理逻辑,它会委派给它的下一级控制器Page Controller去实现,即org.springframework.web.servlet.mvc.Controller
。
1.2 HandlerMapping
既然DispatcherServlet
是整个框架的Front Controller,将它注册到web.xml
时,就注定了它要服务于规定的一组Web请求的命运,而不是单独的一个Web请求。在Spring MVC中,为了灵活处理请求的URL和对应控制器的匹配,引入了org.springframework.Web.servlet.HandlerMapping
来专门管理Web请求到具体的HandlerMapping实例,以获取对应当前Web请求的具体处理类,即Controller
。
1.3 Controller
Controller
是对应DispatcherServlet
的次级控制器,它本身实现了对应某个具体Web请求的处理逻辑。在我们所使用的HandlerMapping
查找到当前Web请求对应哪个Controller
的具体实例之后,DispatcherServlet
即可获得HandlerMapping
所返回的结果,并调用Controller
的处理方法来处理当前的Web请求。
在Controller
的处理方法执行完毕之后,将返回一个org.springframework.Web.servlet.ModelAndView
实例,ModelAndView
包含了如下两部分信息:
- 视图的逻辑名称(或者具体的视图实例)。
DispatcherServlet
将根据该视图的逻辑名称,来决定为用户显示哪个视图。 - 模型数据。视图渲染过程中需要将这些模型数据并入视图的显示中。
有了这两个信息之后,DispatcherServlet
就可以着手视图的渲染工作了。
1.4 ViewResolver和View
我们知道,现在可用的视图技术不止JSP一家,Freemarker、Thymleaf等通用的模板引擎,都可以帮助我们构建相应的视图。鉴于此,Spring提出了一套基于ViewResolver
和View
接口的Web视图处理抽象层,以屏蔽Web框架在使用不同的Web视图技术时候的差异性。DispatcherServlet
只需要根据Controller
处理完毕后通过ModelAndView
返回的逻辑视图名称查找到具体的View
实现,然后委派该具体的View
实现类来根据模型数据,输出具体的视图内容即可。不过,DispatcherServlet
需要依赖一个ViewResolver
将根据ModelAndView
中的逻辑视图名查找相应的View
实现类,然后将查找的结果返回给DispatcherServlet
,DispatcherServlet
最终会将ModelAndView
中的模型数据交给返回的View
实例来处理最终的视图渲染工作。
我们可以通过下图来总览各个组件的流程:
2 近距离接触Spring MVC主要角色
2.1 忙碌的协调人HandlerMapping
HandlerMapping
帮助DispatcherServlet
进行Web请求的URL到具体处理类的匹配。之所以称为HandlderMapping
是因为,Spring MVC中,并不局限于使用Controller
作为次级控制器,还可以使用其他类型的次级控制器,包括Spring MVC提供的除了Controller
之外的次级控制器类型,或者第三方开发框架中的Page Controller组件,所有这些次级控制器类型,在Spring MVC中都统称为Handler。
我们来看一下HandlerMapping
接口的定义:
public interface HandlerMapping {
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
HandlerExecutionChain getHandler(HttpServletRequest var1) throws Exception;
}
我们可以看到返回的是一个HandlerExecutionChain
对象,而不是一个Controller
。目前为止,只需要大家知道,HandlerExecutionChain
中确实包含了用于处理具体Web请求的Handler
,仅此而已。
Spring MVC默认提供了多个HandlerMapping
的实现供我们选用。例如:
BeanNameUrlHandlerMapping
。根据请求路径查询拥有与之相同的beanName的Handler。SimpleUrlHandlerMapping
。它解耦了请求URL与Handler的beanName之间的关系,可以进行独立映射配置。
需要注意的是,我们可以为DispatcherServlet
提供多个HandlerMapping
供其使用。DispatcherServlet
在选用HandlerMapping
的时候,将根据我们所指定的一系列HandlerMapping
的优先级进行排序,然后优先使用优先级高的HandlerMapping
。如果当前的HandlerMapping
可以返回可用的Handler,则不再询问其他的HandlerMapping
。Spring MVC中所有的HandlerMapping
都实现了Ordered
接口,用以指定优先级。
2.2 我们的亲密伙伴Controller
Controller是Spring MVC框架支持的用于处理具体Web请求的handler类型之一。要实现一个具体的Controller,我们当然可以直接实现Controller
接口,其定义如下:
public interface Controller {
ModelAndView handleRequest(HttpServletRequest var1, HttpServletResponse var2) throws Exception;
}
但是更多时候,我们可能会寻求使用更细粒度的Controller框架类。直接实现Controller
接口当然没问题,这让我们可以随心所欲地实现Web处理过程中的所有关注点,但这通常需要我们关注更多底层的细节,比如请求参数的抽取,请求编码的设定等。而实际上,这些关注点可能是所有Controller都需要的,我们就想到可以让这些通用的逻辑复用。这也是Spring MVC提供一系列Controller实现体系的原因。下图给出了Spring MVC中Controller的继承层次体系:
为了便于理解,我们不妨把Controller分为两类。
- 自由挥洒派:与Servlet类似的风格,从
HttpServletRequest
中获取参数,然后验证,调用业务层逻辑,最终返回一个ModelAndView
,甚至你都可以通过HttpServletResponse
输出最终视图。不过,自由虽然自由,但是如果需要处理的底层细节太多,不如求助一下下面将要介绍的规范操作派Controller! - 规范操作派:以
BaseCommandController
为首的规范操作一派,对Web处理过程中的某些通用逻辑做了进一步规范化处理,规范化的方面主要包括:1)自动抽取请求参数并绑定到指定的Command
对象 2)提供了统一的数据验证方式,BaseCommandController
及其子类可以接收一组org.springframework.validation.Validator
以进行数据验证,我们可以根据具体数据提供相应的Validator
实现。3)规范化了表单的处理流程,并且对简单的多页面表单请求处理提供支持。
题外话:你知道Spring中的数据绑定和数据验证吗?
数据绑定:在Web应用程序中使用数据绑定的最大好处就是,我们再也不用自己通过request.getParameter(String)方法遍历获取每个请求参数,然后根据需要转型为自己需要的类型了。Spring MVC提供的数据绑定功能能帮助我们自动提取HttpServletRequest
中的相应参数,然后转型为需要的对象类型。我们唯一需要做的,就是为数据绑定提供一个目标对象Command
(我们希望它一般是JavaBean类型),此后的Web处理逻辑直接同数据绑定完成的Command
对象打交道即可。
对于BaseCommandController
及其子类来说,我们可以通过它们的commandClass
属性设置数据绑定的目标Command
对象类型,如下所示:
<bean id="commandController" class="..AnySubClassOfBaseCommandController">
<property name="commandClass" value="..Command">
</bean>
或者直接在子类的构造方法中直接设定,如下所示:
public class BindingDemoController extends SimpleFormController {
public BindingDemoController() {
setCommandClass(Command.class);
// 进行其他必要设置
}
}
有关数据绑定的过程,可以简单概括如下:
- 在Web请求到达之后,SpringMVC某个框架类将提取当前Web请求中的所有参数名称,然后遍历它,以获取对应每个参数的值,获取的参数名与参数值通常放入一个值对象(
PropertyValue
)中。最终我们将拥有所有需要绑定的参数和参数值的一个集合。 - 有了即将绑定到目标
Command
对象的数据来源之后,我们即可将这些数据根据Command
对象中的各个域属性定义的类型进行数据转型,然后设置到Command
对象上。在这个过程中我们将碰到老朋友BeanWrapperImpl
,它可以用来包裹一个JavaBean对象用来对其进行一些参数的设置:
BeanWrapper beanWrapper = new BeanWrapperImpl(command);
然后比照参数名与Command
对象的属性对应关系,以进行参数值到Command
属性的设置,而参数值与Command
对象属性间类型差异性(字符串/Object)转化的工作,则由BeanWrapperImpl
所持有的自定义PropertyEditor
负责。如果BeanWrapperImpl
所使用的默认的PropertyEditor
没有提供对某一种类型的支持,我们也可以添加自定义的PropertyEditor
。我们来看一个例子,该例子阐述了参数名称与对象属性之间的对应关系:
public class CustomerMetadata {
private String address;
private String zipCode;
private List<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
public CustomerMetadata() {
phoneNumbers.add(new PhoneNumber());
}
// getter和setter方法定义
}
public class PhoneNumber {
private String areaCode;
private String number;
public String getAreaCode() {
return areaCode;
}
public void setAreaCode(String areaCode) {
this.areaCode = areaCode;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
// toString()等方法定义
}
那么为了能够让请求参数对应到Command
对象的相应属性,我们需要按照如下格式定义需要提交的Form表单:
<input type="text" name="address"/>
<input type="text" name="zipCode"/>
<input type="text" name="phoneNumbers[0].number"/>
也就是说,参数的名称对应Command
对应的属性名称。对于绑定的表达式,可以进一步参考Spring的文档。
数据验证:Spring框架提供的数据验证支持并不只是局限于Spring MVC内部使用,从数据验证类所在的包名就能看出来,即org.springframwork.validation
。只要愿意,我们完全可以在独立运行的程序中使用Spring的数据验证功能。
Spring的数据验证框架核心类为org.springframework.validation
和org.springframework.Errors
,Validator
负责实现具体的验证逻辑,而Errors
负责承载 验证过程中出现的错误信息,二者之间的纽带则是Validator
接口定义的主要验证方法validate(target, errors)
。我们来看一下Validator
接口的定义:
public interface Validator {
boolean supports(Class clazz);
void validate(Object target, Errors errors);
}
Validator
具体实现类可以在执行验证逻辑的过程中,随时将验证中的错误信息添加到通过方法参数传入的Errors
对象内,这样,验证逻辑执行完之后,我们就可以通过Errors
检索验证结果了。至于Validator
接口中的support(Class)
方法定义,是为了进一步限定Validator
实现类的职责,除非你想让所有类型数据的验证都让同一个Validator
实现类来做!
至于Validator
内部的具体实现方式,我们暂且不讨论,我们接下来关注的是如何使用Validator
实现类:
CustomerMetaDataValidator validator = new CustomerMetaDataValidator(new PhoneNumberValidator());
CustomerMetadata md = new CustomerMetadata();
BindException errors = new BindException(md, "customerMd");
// 或者直接调用validator.validate()
ValidationUtils.invokeValidator(validator, md, errors);
assertTrue(errors.hasErrors());
Map map = errors.getBindingResult().getModel();
BindingResult result = (BindingResult)map.get("org.springframework.validation.BindingResult,customerMd");
我们只需要构造一个具体的Command
对象示例以及一个Errors
实例,然后通过ValidationUtils
调用对应的Validator
实现类。调用完成之后,如果存在验证错误,我们可以遍历之前传入的errors以获取相应的错误信息,根据具体场景作后续的处理。
至于这些Validator
实现类的执行以及错误处理流程,将由BaseCommandController
及其子类接管,我们要做的只是通过相应的setter方法为其提供Validator
实现。
在实际项目开发过程中,除了通过以上编程方式实现数据验证工作,还可以借助于Commons Validator实现声明式的数据验证。
2.3 ModelAndView
通常,Controller将在Web请求处理完成后,返回一个ModelAndView实例(如果返回null的话,说明Controller
内部自行处理视图的渲染)。该ModelAndView
实例将包含两部分内容,一部分为视图相关内容,可以是逻辑视图名称,也可以是具体的View
实例;另一部分则是模型数据,视图渲染过程中将会把这些模型数据合并入最终的视图输出。我们来看一下它的构造方法:
public ModelAndView(String viewName)
public ModelAndView(String viewName, Map model);
public ModelAndView(String viewName, String modelName, Object modelObject);
public ModelAndView(View view);
public ModelAndView(View view, Map model);
public ModelAndView(View view, Srting modelName, Object modelObject);
2.3.1 ModelAndView中的视图信息
恰如两组构造方法所表明的那样,ModelAndView
可以以逻辑视图名的形式或者View
实例的形式来保存视图信息。如果ModelAndView
中直接返回了具体的View
实例,那么DispatcherServlet
将直接从ModelAndView
中获取该实例并渲染视图(不建议),否则将寻求ViewResolver
的帮助,根据ModelAndView
中的逻辑视图名称获取一个可用的View
实例,然后再渲染视图。
2.3.2 ModelAndView中的模型数据
ModelAndView
以org.springframework.ui.ModelMap
的形式来保持模型数据,通过构造方法传入或者通过实例方法添加的模型数据都将添加到这个ModelMap
中。至于ModelMap
中保持的模型数据将会在视图渲染阶段,由具体的View
实现类来获取并使用。
我们需要为添加到ModelAndView
的一组或多组模型数据提供相应的键,以便View
实现类可以根据这些键来获取数据,然后公开给视图模板。通常,模型中的键和视图模板中的标志符是相对应的,如下:
// ModelAndView
Mav.addObject("monthsOfYear", monthsOfYear);
// 视图渲染
<c: forEach items="${monthsOfYear}" var="month">
..
</c:forEach>
2.4 视图定位器ViewResolver
我们已经知道了ViewResolver
的主要职责是,根据Controller
所返回的ModelAndView
中的逻辑视图名,为DispatcherServlet
返回一个可用的View
实例。ViewSolver
接口:
public interface viewResolver {
View resolverViewName(String viewName, Locale locale) throws Exception;
}
实现类只需要根据resolveViewName()
方法中以参数形式传入的逻辑视图名(viewName
)和当前的Locale
的值,返回相应的View
实例即可。
PS:同时传入Locale的目的是,在需要的情况下,可以根据Locale的不同返回不同的视图实例,这对于支持国际化的视图是必要的。