Spring MVC各组件近距离接触--中--03

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC各组件近距离接触--中--03相关的知识,希望对你有一定的参考价值。

Spring MVC各组件近距离接触--中--03


前言


前面一节,我们已经把自由挥洒派的两个类进行了详细的介绍,下面我们来看看规范操作派。

Spring MVC各组件近距离接触–上–02


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数据绑定之DataBinder篇—01

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数据绑定之DataBinder篇—01

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的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC各组件近距离接触--下--04

一个 ibeacon(信标)可以模仿 NFC 的近距离发送通知吗? [关闭]

近距离看GPU计算

近距离看GPU计算

近距离看GPU计算

小尺寸近距离光学定位仪 - PST Pico