Spring MVC各组件近距离接触--中--03
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC各组件近距离接触--中--03相关的知识,希望对你有一定的参考价值。
Spring MVC各组件近距离接触--中--03
前言
前面一节,我们已经把自由挥洒派的两个类进行了详细的介绍,下面我们来看看规范操作派。
SimpleFormController
作为规范操作派当门大弟子,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中有两个比较重要的点,需要我们的关注:
- 对于不能通过数据验证逻辑的属性域,最基本的做法是通过Errors对象的rejectValue方法将其添加到Errors对象,不过,如果对应的某个对象域的验证仅限于是否为空的话,我们也可以使用ValidatorUtils这个工具类提供的rejectIfEmpty方法来达到同样的目的。
- 如果要对当前对象的嵌套属性域进行验证,我们需要在调用对应的嵌套对象的Validator实现类之前,调用Errors的pushNestedPath方法来明确当前被验证对象的上下文路径,并且在调用之后,通过popNestedPath恢复之前的上下文路径。否则,当Errors对象绑定对应的嵌套对象属性的错误信息的时候,会认为该属性是上层目标对象上的属性,这时就会出现绑定上的异常了。
如果我们不使用pushNestedPath方法,Errors在记录money对应的错误信息的时候,同时需要记录对应该属性的值,那么它就会根据当前属性域对应的表达式到Command对象上获取。可以当它根据money到Customer上查找时,发现Customer对象上不存在一个叫做money的属性域,自然就会抛出异常。
可以,如果在此之前,我们通过pushNestedPath方法改变Errors注册属性域错误信息所使用的上下文路径,比如,变成shopCard[0],那么,当Errors注册money对应的错误信息的时候,就会以shopCard[0].money到Customer获取对应的属性值,那么自然就没有问题了。
在Spring mvc中,以上Validator实现类的执行以及后继错误信息的处理,将由BaseCommandController或者其子类接管,用户不需要操心,我们需要做的,就是设置相关的Validator到BaseCommandController
深入表单form处理流程
SimpleFormController及其父类AbstractFormController最主要的一个特定就是对表单的处理流程进行了统一。
AbstractFormController以模板方法模式从顶层界定了主体上的流程处理逻辑,而处理流程中某些特定动作则留给了子类实现。
以模板方法模式实现的整个流程控制,并非真得就像模板那样死板,我们可以通过覆写其中某些方法以天津自定义的行为逻辑,体现了整个流程的可扩展性和灵活性。
整个规范操作派还是起始于BaseCommandController,因此还是从该顶层类开始讲起:
无论是规范操作派,还是自由挥洒派,在Spring 4之后,基本都被移除了,转而被更加高效和现代化的controller体系所替代,我们后面再说
BaseCommandController—将数据绑定和校验结合在一起
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;
- createBinder方法创建DataBinder的过程也值得一看
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
负责规定好表单处理的模板化流程,以及相关扩展接口。
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<以上是关于Spring MVC各组件近距离接触--中--03的主要内容,如果未能解决你的问题,请参考以下文章