前面一节,我们已经把自由挥洒派的两个类进行了详细的介绍,下面我们来看看规范操作派。
作为规范操作派当门大弟子,SimpleFromController首先继承了BaseCommandController的自动数据绑定和通过Validator的数据验证功能。
其次AbstractFormController在BaseCommandController的基础上,发展了一套模板化的form处理流程。
至此,从数据的封装,验证,到处理流程的模板化,整个规范化体系基本建立完成。
而SimpleFromController和AbstractWizardFormController就被纳入了这个体系之中。
SimpleFromController面向单一表单的处理,而AbstractWizardFormController则提供多页面的交互能力。
要使用SimpleFromController来简化Web请求处理,就需要先了解SimpleFromController具有的数据绑定,验证和流程模板化的三样功能。
在Web环境下使用数据绑定的最主要的好处就是,可以免于自己手动去request中取出请求参数,然后转换为自己需要的类型。
Spring提供了一套完整的数据绑定机制,来帮我们自动提取HttpServletRequest中的相应参数,然后转换为需要的对象类型。
我们唯一需要做的就是为Spring提供一个目标对象,这个目标对象在Spring中被称为Command对象,此后Web处理程序直接同数据绑定完成的command对象打交道即可。
对于BaseCommandController及其子类来说,我们可以通过它们的commandClass属性设置数据绑定的目标Command对象类型:
Spring mvc中关于数据绑定的工作,是由DataBinder及其子类负责完成的:
Spring数据绑定之 WebDataBinder、ServletRequestDataBinder、WebBindingInitializer…—02
这里就不多展开了
Spring数据校验的支持并不仅仅局限于Spring mvc内部使用,从数据验证类所在的包名: org.springframework.validation. 只要原因,我们完全可以在独立运行的应用程序中使用Spring的数据验证功能。
Spirng数据验证框架核心类为org.springframework.validation.Validator和org.springframework.validation.Errors.
Validator负责实现具体的验证逻辑,而Errors负责承载验证过程中出现的错误信息,二者之间的纽带则是Validator接口定义的主要验证方法:
// 注意:它可不是Spring3后才推出的 最初就有
public interface Validator {
// 此clazz是否可以被validate
boolean supports(Class<?> clazz);
// 执行校验,错误消息放在Errors 装着
// 可以参考ValidationUtils这个工具类,它能帮助你很多
void validate(Object target, Errors errors);
}
Validator具体实现类可以在执行验证逻辑的过程中,随时将验证中的错误信息添加到Errors对象内部,这样,在验证逻辑执行完成之后,就可以通过Errors检索验证结果了。
至于Validator接口中的support方法定义,是为了进一步限定Validator实现类的职责,避免所有验证逻辑都交给一个Validator实现类完成。
通常情况下,Spring提供的这个Validator接口,都是为了适配原生Java Bean Validation规范而产生的,而Java Bean Validation规范实现中,我们最常使用的就是hibernate-validator。
下面我们先通过一个完整的例子,演示一遍数据绑定和数据校验的工作流程:
@Data
public class Customer {
private String address;
private String name;
private List<ShopCard> shopCard;
}
@Data
public class ShopCard {
private Integer money;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endTime;
}
/**
* @author 大忽悠
* @create 2022/7/28 11:49
*/
public class CustomerValidator implements Validator {
private final ShopCardValidator shopCardValidator=new ShopCardValidator();
@Override
public boolean supports(Class<?> clazz) {
return ClassUtils.isAssignable(clazz,Customer.class);
}
@Override
public void validate(Object target, Errors errors) {
Customer customer = (Customer) target;
ValidationUtils.rejectIfEmpty(errors,"name","name.empty");
ValidationUtils.rejectIfEmpty(errors,"address","address.empty");
for (int i = 0; i < customer.getShopCard().size(); i++) {
errors.pushNestedPath("shopCard["+i+"]");
ValidationUtils.invokeValidator(shopCardValidator,customer.getShopCard().get(i),errors);
errors.popNestedPath();
}
}
public static class ShopCardValidator implements Validator{
@Override
public boolean supports(Class<?> clazz) {
return ClassUtils.isAssignable(clazz,ShopCard.class);
}
@Override
public void validate(Object target, Errors errors) {
ShopCard shopCard = (ShopCard) target;
if(shopCard.getEndTime().isAfter(LocalDate.now())){
errors.reject("errorCode.shopCard.is.error");
errors.rejectValue("endTime","endTime.is.error","购物卡截止时间有误");
}
if(shopCard.getMoney()<0){
errors.rejectValue("money","money.not.negative");
}
}
}
}
@Test
public void test() throws BindException {
HttpServletRequest request = getRequest();
Customer customer = new Customer();
ServletRequestDataBinder requestDataBinder = new ServletRequestDataBinder(customer, "顾客");
doBind(request, requestDataBinder);
doValidate(requestDataBinder);
//判断是否绑定过程是否产生了错误
requestDataBinder.close();
System.out.println(customer);
}
private void doValidate(ServletRequestDataBinder requestDataBinder) {
requestDataBinder.addValidators(new CustomerValidator());
requestDataBinder.validate();
}
private void doBind(HttpServletRequest request, ServletRequestDataBinder requestDataBinder) throws BindException {
//注册相关默认日期类型转换器---包括解析 @DateTimeFormat注解的
requestDataBinder.setConversionService(new DefaultFormattingConversionService());
requestDataBinder.bind(request);
}
private HttpServletRequest getRequest() {
//需要添加spring-test依赖支持
MockHttpServletRequest mockReq = new MockHttpServletRequest();
mockReq.addParameter("address","翻斗大街--->花园村--->520号");
mockReq.addParameter("name","胡图图");
mockReq.addParameter("shopCard[0].money","-1");
mockReq.addParameter("shopCard[0].endTime","2050-01-02");
mockReq.addParameter("shopCard[1].money","-2");
mockReq.addParameter("shopCard[1].endTime","2030-01-02");
return mockReq;
}
测试结果如下:
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 6 errors
Error in object '顾客': codes [errorCode.shopCard.is.error.顾客,errorCode.shopCard.is.error]; arguments []; default message [null]
Field error in object '顾客' on field 'shopCard[0].endTime': rejected value [2050-01-02]; codes [endTime.is.error.顾客.shopCard[0].endTime,endTime.is.error.顾客.shopCard.endTime,endTime.is.error.shopCard[0].endTime,endTime.is.error.shopCard.endTime,endTime.is.error.endTime,endTime.is.error.java.time.LocalDate,endTime.is.error]; arguments []; default message [购物卡截止时间有误]
Field error in object '顾客' on field 'shopCard[0].money': rejected value [-1]; codes [money.not.negative.顾客.shopCard[0].money,money.not.negative.顾客.shopCard.money,money.not.negative.shopCard[0].money,money.not.negative.shopCard.money,money.not.negative.money,money.not.negative.java.lang.Integer,money.not.negative]; arguments []; default message [null]
Error in object '顾客': codes [errorCode.shopCard.is.error.顾客,errorCode.shopCard.is.error]; arguments []; default message [null]
Field error in object '顾客' on field 'shopCard[1].endTime': rejected value [2030-01-02]; codes [endTime.is.error.顾客.shopCard[1].endTime,endTime.is.error.顾客.shopCard.endTime,endTime.is.error.shopCard[1].endTime,endTime.is.error.shopCard.endTime,endTime.is.error.endTime,endTime.is.error.java.time.LocalDate,endTime.is.error]; arguments []; default message [购物卡截止时间有误]
Field error in object '顾客' on field 'shopCard[1].money': rejected value [-2]; codes [money.not.negative.顾客.shopCard[1].money,money.not.negative.顾客.shopCard.money,money.not.negative.shopCard[1].money,money.not.negative.shopCard.money,money.not.negative.money,money.not.negative.java.lang.Integer,money.not.negative]; arguments []; default message [null]
完美 !
这里对上面的过程进行一下简单的解释:
CustomerValidator实现很简单,具体的校验逻辑通常是针对两种数据实体,一种就是被验证对象本身,另一种就是被验证对象的相应属性。
如果被验证对象本身都不能通过验证,那么,这种错误被称为Global Error,这时,我们使用Errors的reject(String…)这组方法,向Errors中添加相应的错误信息。
如果被验证对象的某个属性域不能够通过验证,那么,我们称这种错误为Field Error,这时,我们使用Errors的rejectValue(String,String…)这组方法向Errors中添加相应的错误信息。
reject(String…)方法第一个参数是错误信息对应的errorCode,而rejectValue(String,String…)方法第一个参数是未能通过验证的属性域的名称,第二个参数才是对应错误信息的errorCode.
ShopCardValidator中有两个比较重要的点,需要我们的关注:
如果我们不使用pushNestedPath方法,Errors在记录money对应的错误信息的时候,同时需要记录对应该属性的值,那么它就会根据当前属性域对应的表达式到Command对象上获取。可以当它根据money到Customer上查找时,发现Customer对象上不存在一个叫做money的属性域,自然就会抛出异常。
可以,如果在此之前,我们通过pushNestedPath方法改变Errors注册属性域错误信息所使用的上下文路径,比如,变成shopCard[0],那么,当Errors注册money对应的错误信息的时候,就会以shopCard[0].money到Customer获取对应的属性值,那么自然就没有问题了。
在Spring mvc中,以上Validator实现类的执行以及后继错误信息的处理,将由BaseCommandController或者其子类接管,用户不需要操心,我们需要做的,就是设置相关的Validator到BaseCommandController
SimpleFormController及其父类AbstractFormController最主要的一个特定就是对表单的处理流程进行了统一。
AbstractFormController以模板方法模式从顶层界定了主体上的流程处理逻辑,而处理流程中某些特定动作则留给了子类实现。
以模板方法模式实现的整个流程控制,并非真得就像模板那样死板,我们可以通过覆写其中某些方法以天津自定义的行为逻辑,体现了整个流程的可扩展性和灵活性。
整个规范操作派还是起始于BaseCommandController,因此还是从该顶层类开始讲起:
无论是规范操作派,还是自由挥洒派,在Spring 4之后,基本都被移除了,转而被更加高效和现代化的controller体系所替代,我们后面再说
BaseCommandController内部提供了对DataBinder进行配置的一些选项和目标对象,以及校验器管理了:
//目标对象名和对应class对象,用来反射初始化
public static final String DEFAULT_COMMAND_NAME = "command";
private String commandName = DEFAULT_COMMAND_NAME;
private Class commandClass;
//校验器数组
private Validator[] validators;
//是否在数据绑定结束后,进行数据校验
private boolean validateOnBinding = true;
//关于DataBinder一些配置
private MessageCodesResolver messageCodesResolver;
private BindingErrorProcessor bindingErrorProcessor;
private PropertyEditorRegistrar[] propertyEditorRegistrars;
private WebBindingInitializer webBindingInitializer;
BaseCommandController中只有一个核心方法bindAndValidate,该方法也是一个模板方法,大家值得学习:
BaseCommandController并没有覆写父类的handleRequestInternal方法,对应的方法会由子类覆写,因此父类提供的bindAndValidate核心方法,会在子类中被调用
//同时完成数据绑定和校验
protected final ServletRequestDataBinder bindAndValidate(HttpServletRequest request, Object command)
throws Exception {
//创建DataBinder
ServletRequestDataBinder binder = createBinder(request, command);
//BindException就是一个简单的代理类,所有方法的实现全部由传入的BindingResult完成,只不过该类实现了Exception接口
BindException errors = new BindException(binder.getBindingResult());
//是否阻止数据绑定,默认是flase
if (!suppressBinding(request)) {
//进行数据绑定
binder.bind(request);
//留给子类的扩展接口
onBind(request, command, errors);
//isValidateOnBinding判断是否要在数据绑定结束后进行数据校验,默认为true
//suppressValidation是否阻止当前的数据校验,默认false
if (this.validators != null && isValidateOnBinding() && !suppressValidation(request, command, errors)) {
//进行数据校验
for (int i = 0; i < this.validators.length; i++) {
ValidationUtils.invokeValidator(this.validators[i], command, errors);
}
}
//扩展接口
onBindAndValidate(request, command, errors);
}
return binder;
}
protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object command)
throws Exception {
ServletRequestDataBinder binder = new ServletRequestDataBinder(command, getCommandName());
//BaseCommandController内部提供的配置属性,当然要在这里决定是否起作用喽!
prepareBinder(binder);
//交给WebBindingInitializer决定,是否要对Binder进行一波配置更改
initBinder(request, binder);
return binder;
}
protected final void prepareBinder(ServletRequestDataBinder binder) {
if (useDirectFieldAccess()) {
binder.initDirectFieldAccess();
}
if (this.messageCodesResolver != null) {
binder.setMessageCodesResolver(this.messageCodesResolver);
}
if (this.bindingErrorProcessor != null) {
binder.setBindingErrorProcessor(this.bindingErrorProcessor);
}
if (this.propertyEditorRegistrars != null) {
for (int i = 0; i < this.propertyEditorRegistrars.length; i++) {
this.propertyEditorRegistrars[i].registerCustomEditors(binder);
}
}
}
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
if (this.webBindingInitializer != null) {
this.webBindingInitializer.initBinder(binder, new ServletWebRequest(request));
}
}
这部分不清楚的,说明对DataBinder数据绑定体系结构不了解,可以先去了解一下,再回看:
Spring数据绑定之 WebDataBinder、ServletRequestDataBinder、WebBindingInitializer…—02
模板方法模式固定基本流程 + 扩展接口: 极大提高框架的可扩展性
AbstractFormController
负责规定好表单处理的模板化流程,以及相关扩展接口。
AbstractFormController
实现了handleRequestInternal
方法,所以一切的一切,都要从该方法讲起,这里表单处理模板化的入口:
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
//判断当前请求是否是表单处理请求----如果是POST请求,就默认为true
if (isFormSubmission(request)) {
// Fetch form object from HTTP session, bind, validate, process submission.
try {
//反射获取目标对象
Object command = getCommand(request);
//绑定然后进行校验
ServletRequestDataBinder binder = bindAndValidate(request, command);
//用于存放错误信息
BindException errors = new BindException(binder.getBindingResult());
//处理表单提交---子类实现
return processFormSubmission(request, response, command, errors);
}
catch (HttpSessionRequiredException ex) {
// Cannot submit a session form if no form object is in the session.
if (logger.isDebugEnabled()) {
logger.debug("Invalid submit detected: " + ex.getMessage());
}
//如果出现异常,那么进行处理
//这里是再次尝试去提交一遍表单
return handleInvalidSubmit(request, response);
}
}
else {
// New form to show: render form view.
//渲染表单----最终调用到showForm方法
return showNewForm(request, response);
}
}
AbstractFormController模板化流程简单概括就是:
分别是处理单表单和多表单提交流程的。
因为AbstractFormController及其子类的实现及源码就不展开讲了,因为这部分内容已经过时了,我觉得我们重点学习一下它的设计思想就行了,而核心的设计思想其实体现在父类AbstractFormController的handleRequestInternal方法中。
我们已经了解了controller两大门派的几位主要弟子,虽然这两大门派都已经被淘汰了,但是思想是不断递进了,再后面学习新的Controller体系时,可以前后对比。
Spring MVC旧的controller体系的中还有几个用于不同目的实现类,下面可以来看看:
AbstractCommandController是规范操作派里面的少数派,它继承了BaseCommandController的数据绑定以及数据验证的功能支持,自身也定义了简单的请求处理流程,但它的主要目的不是面向表单的处理(该方法的功能已经由AbstractFormController家族接管了)。
实际上,我们可以认为AbstractCommandController只是一个加强型的AbstractController,直接继承AbstractController与直接继承AbstractCommandController差不多,唯一的区别就是后者进一步规范了参数的获取和验证操作。
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
//反射实例化对象
Object command = getCommand(request);
//数据绑定和验证
ServletRequestDataBinder binder = bindAndValidate(request, command);
//错误记录
BindException errors = new BindException(binder.getBindingResult());
//留给子类实现的抽象方法
return handle(request, response, command, errors);
}
//抽象方法
protected abstract ModelAndView handle(
HttpServletRequest request, HttpServletResponse response, Object command, BindException errors)throws Exception;
继承AbstractCommandController,只需要实现handle模板方法即可,因为之前数据绑定和验证都已经搞定了。
这就好比继承AbstractController,我们只需要实现一个handleRequestInternal模板方法,而像页面缓存的设置等功能,则由AbstractController这个父类完成。
总之,通过继承AbstractCommandController实现一个Controller,我们只需要关心数据绑定和验证之后的处理逻辑的实现即可。
ParameterizableViewController的实现逻辑十分简单,或者干脆就没有处理逻辑,只是返回一个包含指定逻辑视图名的ModelAndView实例。
public class ParameterizableViewController extends AbstractController {
private String viewName;
public void setViewName(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return this.viewName;
}
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
return new ModelAndView(getViewName(), RequestContextUtils.getInputFlashMap(request));
}
}
该Controller实现类的主要目的是为了帮助我们将对具体视图的直接请求纳入到Spring mvc的统一处理体系之下,这样,我们依赖于ViewResolver的映射能力向客户端屏蔽不同视图类型的差异性。
比如,如果我们在页面内只是通过超链接访问某个页面(比如/WEB-INF/jsp/help/Help4sth.jsp),期间不需要任何处理,那么我们就可以在特定于DispathcerServlet的WebApplicationContext中添加如下Controller定义:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/hello/**">parameterizableViewController</prop>
</props>
</property>
</bean>
<bean id="parameterizableViewController" class="org.springframework.web.servlet.mvc.ParameterizableViewController">
<property name="viewName" value="jsp/hello"></property>
</bean>
通过viewName指定具体的视图名,parameterizableViewController将不做任何处理,直接将对应满足/hello/**格式的请求导向指定的视图。
InternalResourceViewResolver和SimpleUrlHandlerMapping前面两节简单介绍过了,不清楚可以看一下,后面也会对各个组件源码进行详细分析
使用ParameterizableViewController一次只能映射一个视图文件,如果有一组视图文件都需要不做任何处理直接返回的话,我们就得使用urlfilenameviewcontroller。
假设我们的帮助页面都存放在/WEB-INF/jsp/help/目录下,为了免去一个页面配置一个Controller之苦,我们就可以在特定于DispathcerServlet的WebApplicationContext中添加一个Urlfilenameviewcontroller的定义,让它全权管理对帮助页面的访问。
我们先来欣赏一下urlfilenameviewcontroller的源码,在给出一个具体实际例子作为演示吧:
父类AbstractUrlViewController中实现了核心方法handleRequestInternal,因此对于父类方法而言,只需要阅读该核心方法,即可一览全局:
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
//确定当前请求的访问路径,这里路径解析涉及到是保留context路径和servlet拦截的full path路径,还是去掉的问题,因此比较麻烦
//但是对于spring mvc提供的DispathcerServlet来说,默认拦截路径为/,我们这里在web.xml中配置的也是'/'
//并且contextPath也是'/'
//因此不需要操心路径的问题
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
//如何获取到viewName是由子类来实现的,这也是核心逻辑
String viewName = getViewNameForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Returning view name '" + viewName + "' for lookup path [" + lookupPath + "]");
}
return new ModelAndView(viewName, RequestContextUtils.getInputFlashMap(request));
}
因此对于子类UrlFilenameViewController来说,核心方法就是确定viewName:
@Override
protected String getViewNameForRequest(HttpServletRequest request) {
//抽取出uri请求路径
//获取AbstractUrlHandlerMapping中exposePathWithinMapping缓存的请求路径
//SimpleUrlHandlerMapping模糊匹配的路径为/hello/** ,而我们访问的路径为/hello/hello
//这里抽取拿到的就是去掉匹配前缀剩余部分,即hello
String uri = extractOperableUrl(request);
//通过请求路径,拿到viewName
return getViewNameForUrlPath(uri);
}
private final Map<String, String> viewNameCache = new ConcurrentHashMap<String, String>(256);
//会缓存请求路径对应的viewName
protected String getViewNameForUrlPath(String uri) {
String viewName = this.viewNameCache.get(uri);
if (viewName == null) {
viewName = extractViewNameFromUrlPath(uri);
viewName = postProcessViewName(viewName);
this.viewNameCache.put(uri, viewName);
}
return viewName;
}
//如果uri是 "/index.html" ,那么抽取后返回的就是index,俗称掐头去尾
protected String extractViewNameFromUrlPath(String uri) {
int start = (uri.charAt(0) == '/' ? 1 : 0);
int lastIndex = uri.lastIndexOf(".");
int end = (lastIndex < 0 ? uri.length() : lastIndex);
return uri.substring(start, end);
}
private String prefix = "";
private String suffix = "";
//后处理viewName,说白了就是加上给上面得到的viewName,加上前后缀
protected String postProcessViewName(String viewName) {
return getPrefix() + viewName + getSuffix();
}
下面是一些请求路径经过UrlFilenameViewController处理后得到的viewName样子:
"/index" -> "index"
"/index.html" -> "index"
"/index.html" + prefix "pre_" and suffix "_suf" -> "pre_index_suf"
"/products/view.html" -> "products/view"
实战:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/hello/**">urlfilenameviewcontroller</prop>
</props>
</property>
</bean>
<bean id="urlfilenameviewcontroller" class="org.springframework.web.servlet.mvc.UrlFilenameViewController">
<property name="prefix" value="jsp/"/>
</bean>
ServletForwardingController是将对当前Controller的请求转发给当前应用中定义的某个servlet,也就是说,将controller与servlet拉低到了同一个水平。
ServletWrappingController则是将当前应用中的某个servlet直接包装为了一个Controller,所有到ServletWrappingController的请求实际上是由它内部所包装的这个servlet来处理的。
这两种controller实现类更多的是为了集成现有的servlet,比如将包含某些现有逻辑的servlet也纳入到spring mvc的处理体系中。