[Re] SpringMVC-4(数据绑定+数据格式化+数据校验)

1. 数据绑定

SpringMVC 封装自定义类型对象的时候,JavaBean 要和页面提交的数据进行一一绑定。但页面提交的数据都是字符串,而服务器端 Java 数据类型各种各样。

牵扯到以下操作:

  1. 数据绑定期间的数据类型转换,如:name=root&age=35
  2. 数据绑定期间的数据格式化,如:日期时间格式化
  3. 数据绑定期间的数据校验,不仅要有前端(JS+正则)校验,也要有后端校验

1.1 源码

新的源码在 ModelAttributeMethodProcessor:

// 工厂创建数据绑定器
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
if (binder.getTarget() != null) { // binder.getTarget() ~ POJO 对象
    // 将页面提交过来的数据封装到 JavaBean 的属性中
    bindRequestParameters(binder, request);
    validateIfApplicable(binder, parameter);
    if (binder.getBindingResult().hasErrors()) {
        if (isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
        }
    }
}

数据绑定器 WebDataBinder 负责数据绑定工作。数据绑定期间产生的类型转换、格式化。

ConversionService 中如下所示,内建了很多 Converter 和 Formatter。不同类型的转换和格式化用它自己的 Converter。

ConversionService converters =
java.lang.Long -> java.lang.String: DateTimeFormatAnnotationFormatterFactory@6434d4f2,NumberFormat java.lang.Long -> java.lang.String: 
java.time.LocalDate -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.LocalDate -> java.lang.String : standard.TemporalAccessorPrinter@2c7927e6
java.time.LocalDateTime -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.LocalDateTime -> java.lang.String : standard.TemporalAccessorPrinter@d6b3e27
java.time.LocalTime -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.LocalTime -> java.lang.String : standard.TemporalAccessorPrinter@35e809fb
java.time.OffsetDateTime -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.OffsetDateTime -> java.lang.String : standard.TemporalAccessorPrinter@c6d02e6
java.time.OffsetTime -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.OffsetTime -> java.lang.String : standard.TemporalAccessorPrinter@64423271
java.time.ZonedDateTime -> java.lang.String: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.time.ZonedDateTime -> java.lang.String : standard.TemporalAccessorPrinter@6166f845
java.util.Calendar -> java.lang.String: DateTimeFormatAnnotationFormatterFactory@6434d4f2
java.util.Date -> java.lang.String: DateTimeFormatAnnotationFormatterFactory@6434d4f2
NumberFormat java.lang.Double -> java.lang.String: 
NumberFormat java.lang.Float -> java.lang.String: 
NumberFormat java.lang.Integer -> java.lang.String: 
NumberFormat java.lang.Short -> java.lang.String: 
NumberFormat java.math.BigDecimal -> java.lang.String: 
NumberFormat java.math.BigInteger -> java.lang.String: 
java.lang.Boolean -> java.lang.String : ObjectToStringConverter@29e583ed
java.lang.Character -> java.lang.Number : CharacterToNumberFactory@4b9fbd0a
java.lang.Character -> java.lang.String : ObjectToStringConverter@67f8467
java.lang.Enum -> java.lang.String : EnumToStringConverter@13c965f6
java.lang.Long -> java.util.Calendar : DateFormatterRegistrar$LongToCalendarConverter@7f0655da
java.lang.Long -> java.util.Date : DateFormatterRegistrar$LongToDateConverter@15b2ed49
java.lang.Number -> java.lang.Character : NumberToCharacterConverter@4a2e1eee
java.lang.Number -> java.lang.Number : NumberToNumberConverterFactory@17fb24cd
java.lang.Number -> java.lang.String : ObjectToStringConverter@5f98d55
java.lang.String -> java.lang.Long: DateTimeFormatAnnotationFormatterFactory@6434d4f2,java.lang.String -> NumberFormat java.lang.Long: 
java.lang.String -> java.time.LocalDate: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.LocalDate: standard.TemporalAccessorParser@3d03b4a0
java.lang.String -> java.time.LocalDateTime: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.LocalDateTime: standard.TemporalAccessorParser@1c9c3989
java.lang.String -> java.time.LocalTime: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.LocalTime: standard.TemporalAccessorParser@7e805010
java.lang.String -> java.time.OffsetDateTime: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.OffsetDateTime: standard.TemporalAccessorParser@7011c3ab
java.lang.String -> java.time.OffsetTime: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.OffsetTime: standard.TemporalAccessorParser@4b8f44ae
java.lang.String -> java.time.ZonedDateTime: standard.Jsr310DateTimeFormatAnnotationFormatterFactory@1de0943c,java.lang.String -> java.time.ZonedDateTime: standard.TemporalAccessorParser@793f7beb
java.lang.String -> java.util.Calendar: DateTimeFormatAnnotationFormatterFactory@6434d4f2
java.lang.String -> java.util.Date: DateTimeFormatAnnotationFormatterFactory@6434d4f2
java.lang.String -> NumberFormat java.lang.Double: 
java.lang.String -> NumberFormat java.lang.Float: 
java.lang.String -> NumberFormat java.lang.Integer: 
java.lang.String -> NumberFormat java.lang.Short: 
java.lang.String -> NumberFormat java.math.BigDecimal: 
java.lang.String -> NumberFormat java.math.BigInteger: 
java.lang.String -> java.lang.Boolean : StringToBooleanConverter@5eec8efa
java.lang.String -> java.lang.Character : StringToCharacterConverter@11dac86
java.lang.String -> java.lang.Enum : StringToEnumConverterFactory@48cb1a15
java.lang.String -> java.lang.Number : StringToNumberConverterFactory@77c9768d
java.lang.String -> java.time.Instant: standard.InstantFormatter@2a5b8feb
java.lang.String -> java.util.Locale : StringToLocaleConverter@33a9f1cc
java.lang.String -> java.util.Properties : StringToPropertiesConverter@66ed1c44
java.lang.String -> java.util.UUID : StringToUUIDConverter@3455abb1
java.time.Instant -> java.lang.String : standard.InstantFormatter@2a5b8feb
java.time.ZoneId -> java.util.TimeZone : ZoneIdToTimeZoneConverter@9d772b8
java.util.Calendar -> java.lang.Long : DateFormatterRegistrar$CalendarToLongConverter@6bc06701
java.util.Calendar -> java.util.Date : DateFormatterRegistrar$CalendarToDateConverter@f5e2e3e
java.util.Date -> java.lang.Long : DateFormatterRegistrar$DateToLongConverter@d3cfc51
java.util.Date -> jav...

1.2 数据绑定流程

SpringMVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是 DataBinder,运行机制如下:

  1. Spring MVC 主框架将 ServletRequest 对象及目标方法的形参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder(数据绑定器) 实例对象。
  2. DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换、数据格式化工作。将 Servlet 中的请求信息填充到形参对象中。
  3. 调用 Validator 组件对已经绑定了请求消息的形参对象进行数据合法性校验,并最终生成数据绑定结果 BindingData 对象。
  4. Spring MVC 抽取 BindingResult 中的形参对象和校验错误对象,将它们赋给处理方法的响应形参。

1.3 自定义类型转换器

ConversionService 是 Spring 类型转换体系的核心接口。

可以通过 ConversionServiceFactoryBean 在 Spring IOC 容器中定义一个 ConversionService。Spring 将自动识别出 IOC 容器中的 ConversionService,并在 Bean 属性配置及 Spring MVC 处理方法形参绑定等场合使用它进行数据的转换。

可通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器。

Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactroyBean 中:

  • Converter<S, T> 将 S 类型对象转为 T 类型对象(使用这种)
  • ConverterFactory
  • GenericConverter

(1) 实现 Converter<S, T>,写一个自定义类型转换器

public class StringToEmpConverter implements Converter<String, Employee>{

    @Autowired
    DepartmentDao deptDao;

    @Override
    public Employee convert(String source) {
        System.out.println("要转换的字符串:" + source);
        Employee emp = new Employee();
        if(source.contains("-")) {
            String[] params = source.split("-");
            emp.setLastName(params[0]);
            emp.setEmail(params[1]);
            emp.setGender(Integer.parseInt(params[2]));
            emp.setDepartment(deptDao.getDepartment(Integer.parseInt(params[3])));
        }
        return null;
    }
}

(2) Converter 是 ConversionService 中的组件,自定义的 Converter 得放进 ConversionService 中。

<!-- 利用 ConversionServiceFactoryBean 在 IOC 容器中定义一个 ConversionService -->
<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <!-- 添加我们自定义的类型转换器 -->
    <property name="converters">
        <set>
            <bean class="cn.edu.nuist.component.StringToEmpConverter"></bean>
        </set>
    </property>
</bean>

(3) 将 WebDataBinder 中的 ConversionService 设置成带有我们自定义 Converter 的 ConversionService。

2. annotation-driven

<mvc:annotation-driven /> 会自动注册 RequestMappingHandlerMapping 、RequestMappingHandlerAdapter 与 ExceptionHandlerExceptionResolver 三个组件。还将提供以下支持:

  • 支持使用 ConversionService 实例对表单参数进行类型转换
  • 支持使用 @NumberFormat annotation、@DateTimeFormat 注解完成数据类型的格式化
  • 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
  • 支持使用 @RequestBody 和 @ResponseBody 注解

通过查看这个解析该标签的类,会发现,它添了好多东西 ...

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
    Object source = parserContext.extractSource(element);

    CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
    parserContext.pushContainingComponent(compDefinition);

    RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);

    RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
    handlerMappingDef.setSource(source);
    handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    handlerMappingDef.getPropertyValues().add("order", 0);
    handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(handlerMappingDef);
    if (element.hasAttribute("enable-matrix-variables") || element.hasAttribute("enableMatrixVariables")) {
        Boolean enableMatrixVariables = Boolean.valueOf(element.getAttribute(
        element.hasAttribute("enable-matrix-variables") ? "enable-matrix-variables" : "enableMatrixVariables"));
        handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables);
    }

    RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);
    RuntimeBeanReference validator = getValidator(element, source, parserContext);
    RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element, source, parserContext);

    RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class);
    bindingDef.setSource(source);
    bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    bindingDef.getPropertyValues().add("conversionService", conversionService);
    bindingDef.getPropertyValues().add("validator", validator);
    bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver);

    ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext);
    ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext);
    ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext);
    String asyncTimeout = getAsyncTimeout(element, source, parserContext);
    RuntimeBeanReference asyncExecutor = getAsyncExecutor(element, source, parserContext);
    ManagedList<?> callableInterceptors = getCallableInterceptors(element, source, parserContext);
    ManagedList<?> deferredResultInterceptors = getDeferredResultInterceptors(element, source, parserContext);

    RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
    handlerAdapterDef.setSource(source);
    handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
    handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
    if (element.hasAttribute("ignore-default-model-on-redirect")
             || element.hasAttribute("ignoreDefaultModelOnRedirect")) {
        Boolean ignoreDefaultModel = Boolean.valueOf(element.getAttribute(
        element.hasAttribute("ignore-default-model-on-redirect")
                 ? "ignore-default-model-on-redirect" : "ignoreDefaultModelOnRedirect"));
        handlerAdapterDef.getPropertyValues().add("ignoreDefaultModelOnRedirect", ignoreDefaultModel);
    }
    if (argumentResolvers != null) {
        handlerAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers);
    }
    if (returnValueHandlers != null) {
        handlerAdapterDef.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers);
    }
    if (asyncTimeout != null) {
        handlerAdapterDef.getPropertyValues().add("asyncRequestTimeout", asyncTimeout);
    }
    if (asyncExecutor != null) {
        handlerAdapterDef.getPropertyValues().add("taskExecutor", asyncExecutor);
    }
    handlerAdapterDef.getPropertyValues().add("callableInterceptors", callableInterceptors);
    handlerAdapterDef.getPropertyValues().add("deferredResultInterceptors", deferredResultInterceptors);
    String handlerAdapterName = parserContext.getReaderContext().registerWithGeneratedName(handlerAdapterDef);

    String uriCompContribName = MvcUriComponentsBuilder.MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME;
    RootBeanDefinition uriCompContribDef = new RootBeanDefinition(CompositeUriComponentsContributorFactoryBean.class);
    uriCompContribDef.setSource(source);
    uriCompContribDef.getPropertyValues().addPropertyValue("handlerAdapter", handlerAdapterDef);
    uriCompContribDef.getPropertyValues().addPropertyValue("conversionService", conversionService);
    parserContext.getReaderContext().getRegistry().registerBeanDefinition(uriCompContribName, uriCompContribDef);

    RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class);
    csInterceptorDef.setSource(source);
    csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService);
    RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class);
    mappedCsInterceptorDef.setSource(source);
    mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null);
    mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef);
    String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedCsInterceptorDef);

    RootBeanDefinition exceptionHandlerExceptionResolver = new 
            RootBeanDefinition(ExceptionHandlerExceptionResolver.class);
    exceptionHandlerExceptionResolver.setSource(source);
    exceptionHandlerExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    exceptionHandlerExceptionResolver.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
    exceptionHandlerExceptionResolver.getPropertyValues().add("messageConverters", messageConverters);
    exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0);
    String methodExceptionResolverName =
    parserContext.getReaderContext().registerWithGeneratedName(exceptionHandlerExceptionResolver);

    RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class);
    responseStatusExceptionResolver.setSource(source);
    responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    responseStatusExceptionResolver.getPropertyValues().add("order", 1);
    String responseStatusExceptionResolverName =
    parserContext.getReaderContext().registerWithGeneratedName(responseStatusExceptionResolver);

    RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class);
    defaultExceptionResolver.setSource(source);
    defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    defaultExceptionResolver.getPropertyValues().add("order", 2);
    String defaultExceptionResolverName =
    parserContext.getReaderContext().registerWithGeneratedName(defaultExceptionResolver);

    parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, methodMappingName));
    parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, handlerAdapterName));
    parserContext.registerComponent(new BeanComponentDefinition(uriCompContribDef, uriCompContribName));
    parserContext.registerComponent(new BeanComponentDefinition(exceptionHandlerExceptionResolver, methodExceptionResolverName));
    parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName));
    parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));
    parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName));

    // Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
    MvcNamespaceUtils.registerDefaultComponents(parserContext, source);

    parserContext.popAndRegisterContainingComponent();

    return null;
}

为什么请求不好使就加 <mvc:default-servlet-handler /><mvc:annotation-driven />

  • 这俩都没加
    • 动态资源(@RequestMapping 映射的资源) 能访问
    • 静态资源(HTML、CSS、JS) 不能访问:由 '能访问动态的原因' 可知,就是因为 handlerMap 中没有保存静态资源映射的请求,所以不能访问
  • 只添加 <mvc:annotation-driven />,只能访问动态,不能访问静态。
  • 只添加 <mvc:default-servlet-handler />
    • 动态资源不能访问,如下所示,处理动态资源映射的 DefaultAnnotationHandlerMapping 被 SimpleUrlHandlerMapping 替换了,它的作用是将所有的请求都交给 tomcat 来处理。
    • 静态资源能访问的原因:正是因为请求都交给了 tomcat 来处理
  • 这俩都加上
    • 如果是请求静态资源,handlerMapping[2] 来处理(交给 tomcat)
    • 如果是请求动态资源 → handlerMapping[0] 来处理,handlerMethods 属性保存了每一个请求用哪个方法来处理

ha.handle(xxx) 确定参数都换成了解析器,不再 for 循环一个个识别参数了。

3. 数据格式化

  • 对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。
  • Spring 在格式化模块中定义了一个实现 ConversionService 接口的 FormattingConversionService 实现类,该实现类扩展了 GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能(ConversionServiceFactoryBean 方式创建的 ConversionService 是没有「格式化器」存在的)。
    public class ConversionServiceFactoryBean ... {
        private Set<?> converters;
    }
    
    public class FormattingConversionServiceFactoryBean ... {
        private Set<?> converters;
        private Set<?> formatters;
    }
    
  • FormattingConversionService 拥有一个 FormattingConversionServiceFactroyBean 工厂类,后者用于在 Spring 上下文中构造前者。
  • FormattingConversionServiceFactroyBean 内部已经注册了 :
    • NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用 @NumberFormat 注解
    • JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用 @DateTimeFormat 注解
  • 装配了 FormattingConversionServiceFactroyBean 后,就可以在 Spring MVC 形参绑定及模型数据输出时使用注解驱动了。 <mvc:annotation-driven/> 默认创建的 ConversionService 实例即为 FormattingConversionServiceFactroyBean。

3.1 日期格式化

@DateTimeFormat 注解可对 java.util.Date、java.util.Calendar、java.long.Long 时间类型进行标注

  • pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:yyyy-MM-dd hh:mm:ss
  • iso 属性
  • style 属性

3.2 数值格式化

@NumberFormat 可对类似数字类型的属性进行标注,它拥有两个互斥的属性:

  • style:类型为 NumberFormat.Style。用于指定样式类型,包括 3 种:Style.NUMBER(正常数字类型)、 Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
  • pattern:类型为 String,自定义样式,如 pattern="#,###"

4. 数据校验

4.1 JSR 303 引入

  • 只做前端校验是不安全的;在重要数据一定要加上后端验证。
  • SpringMVC 可以 JSR 303 来做数据校验,它是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
    • JDBC 规范 → 实现(各个厂商的驱动包)
    • JSR 303 规范 → Hibernate Validator(第三方校验框架)
  • JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
  • Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解

4.2 实现步骤

4.2.1 导包&属性加注解

  • Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。
  • Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验。
  • Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个 LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中。
  • Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下。
    classmate-0.8.0.jar
    jboss-logging-3.1.1.GA.jar
    validation-api-1.1.0.CR1.jar
    hibernate-validator-5.0.0.CR2.jar
    hibernate-validator-annotation-processor-5.0.0.CR2.jar
    
  • <mvc:annotation-driven/> 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的形参上标注 @valid 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作。
  • 给 JavaBean 的属性添加校验注解(注解有个 message 属性,直接定义错误提示信息;但这样就不能 i8n 了)

4.2.2 @Valid & BindingResult

在已经标注了 JSR303 注解的表单/命令对象前标注一个 @Valid,Spring MVC 框架在将请求参数绑定到该形参对象后,就会调用校验框架根据注解声明的校验规则实施校验。

Spring MVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的形参中,这个保存校验结果的形参必须是 BindingResult 或 Errors 类型,这两个类都位于 org.springframework.validation 包中 // BindingResult 扩展了 Errors 接口

FieldError getFieldError(String field)
List<FieldError> getFieldErrors()
Object getFieldValue(String field)
Int getErrorCount()

在目标方法中获取校验结果:在表单/命令对象类的属性中标注校验注解,在处理方法对应的入参前添加 @Valid,Spring MVC 就会实施校验并将校验结果保存在被校验入参对象之后的 BindingResult 或 Errors 入参中。

需校验的 Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的形参!

4.2.3 在页面上显示错误

Spring MVC 除了会将表单/命令对象的校验结果保存到对应的 BindingResult 或 Errors 对象中外,还会将所有校验结果保存到 “隐含模型”。

即使处理方法的签名中没有对应于表单/命令对象的结果入参,校验结果也会保存在 “隐含对象” 中。隐含模型中的所有数据最终将通过 HttpServletRequest 的属性列表暴露给 JSP 视图对象,因此在 JSP 中可以获取错误信息。

在 JSP 页面上可通过 <form:errors path="userName"> 显示错误消息或者自己封装好带过去。

效果展示:

4.3 提示消息的国际化

每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的 FieldError 对象。

当一个属性校验失败后,校验框架会为该属性生成 4 个消息代码,这些代码以校验注解类名为前缀,结合 modleAttribute、属性名及属性类型名生成多个对应的消息代码;国际化文件中错误消息的 key 必须对应一个错误代码

codes [
    Email.employee.email,    校验规则.隐含模型中这个对象的key.对象的属性
    Email.email,             校验规则.属性名
    Email.java.lang.String,  校验规则.属性类型
    Email
];
  • 隐含模型中 employee 对象的 email 属性字段发生了 @Email 校验错误,就会生成 Email.employee.email
  • Email.email:所有的 email 属性只要发生了@Email 错误,...
  • Email.java.lang.String:只要是 String 类型发生了@Email 错误,...
  • Email:只要发生了@Email校验错误,...

当使用 Spring MVC 标签显示错误消息时, Spring MVC 会查看 WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息。

若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:

  • required:必要的参数不存在。如 @RequiredParam("param1") 标注了一个形参,但是该参数不存在
  • typeMismatch:在数据绑定时,发生数据类型不匹配的问题
  • methodInvocation:Spring MVC 在调用处理方法时发生了错误

编写国际化的文件:errors_zh_CN.properties,errors_en_US.properties

注册国际化资源文件

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
   <property name="basename" value="errors"></property>
</bean>
原文地址:https://www.cnblogs.com/liujiaqi1101/p/13674498.html