1. 程式人生 > 實用技巧 >[Re] SpringMVC-4

[Re] SpringMVC-4

資料繫結

SpringMVC 封裝自定義型別物件的時候,JavaBean 要和頁面提交的資料進行一一繫結。但頁面提交的資料都是字串,而伺服器端 Java 資料型別各種各樣。

牽扯到以下操作:

  1. 資料繫結期間的資料型別轉換,如:name=root&age=35
  2. 資料繫結期間的資料格式化,如:日期時間格式化
  3. 資料繫結期間的資料校驗,不僅要有前端(JS+正則)校驗,也要有後端校驗

原理

新的原始碼在 ModelAttributeMethodProcessor:

// 工廠建立資料繫結器
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
if (binder.getTarget() != null) {
    // 將頁面提交過來的資料封裝到 JavaBean 的屬性中
    bindRequestParameters(binder, request);
    validateIfApplicable(binder, parameter);
    if (binder.getBindingResult().hasErrors()) {
        if (isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
        }
    }
}

資料繫結器 WebDataBinder 負責資料繫結工作。資料繫結期間產生的型別轉換、格式化。

ConversionService 中如下所示,內建了很多 Converter。不同型別的轉換和格式化用它自己的 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. Spring MVC 主框架將 ServletRequest 物件及目標方法的形參例項傳遞給 WebDataBinderFactory 例項,以建立 DataBinder 例項物件。
  2. DataBinder 呼叫裝配在 Spring MVC 上下文中的 ConversionService 元件進行資料型別轉換、資料格式化工作。將 Servlet 中的請求資訊填充到形參物件中。
  3. 呼叫 Validator 元件對已經綁定了請求訊息的形參物件進行資料合法性校驗,並最終生成資料繫結結果 BindingData 物件。
  4. Spring MVC 抽取 BindingResult 中的形參物件和校驗錯誤物件,將它們賦給處理方法的響應形參。

自定義型別轉換器

  • 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 介面,寫一個自定義型別轉換器
    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 中
    <!-- 自定義的 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

annotation-driven

  • <mvc:annotation-driven /> 會自動註冊 RequestMappingHandlerMapping 、RequestMappingHandlerAdapter 與 ExceptionHandlerExceptionResolver 三個 bean。
  • 還將提供以下支援:
    • 支援使用 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:default-servlet-handler />
    • 動態資源不能訪問,如下所示,處理動態資源對映的 DefaultAnnotationHandlerMapping 被 SimpleUrlHandlerMapping 替換了,它的作用是將所有的請求都交給 tomcat 來處理。
    • 靜態資源能訪問的原因:正是因為請求都交給了 tomcat 來處理
  • 這倆都加上
    • 如果是請求靜態資源,handlerMapping[2] 來處理(交給 tomcat)
    • 如果是請求動態資源 → handlerMapping[0] 來處理,handlerMethods 屬性儲存了每一個請求用哪個方法來處理

確定引數都換成了解析器,不再 for 迴圈一個個識別了。

資料格式化

  • 對屬性物件的輸入/輸出進行格式化,從其本質上講依然屬於 “型別轉換” 的範疇。
  • Spring 在格式化模組中定義了一個實現 ConversionService 介面的 FormattingConversionService 實現類,該實現類擴充套件了 GenericConversionService,因此它既具有型別轉換的功能,又具有格式化的功能(ConversionServiceFactoryBean 建立的 ConversionService 是沒有格式化器存在的)。
    ConversionServiceFactoryBean {
        private Set<?> converters;
    }
    
    FormattingConversionServiceFactoryBean {
        private Set<?> converters;
        private Set<?> formatters;
    }
    
  • FormattingConversionService 擁有一個 FormattingConversionServiceFactroyBean 工廠類,後者用於在 Spring 上下文中構造前者。
  • FormattingConversionServiceFactroyBean 內部已經註冊了 :
    • NumberFormatAnnotationFormatterFactroy:支援對數字型別的屬性使用 @NumberFormat 註解
    • JodaDateTimeFormatAnnotationFormatterFactroy:支援對日期型別的屬性使用 @DateTimeFormat 註解
  • 裝配了 FormattingConversionServiceFactroyBean 後,就可以在 Spring MVC 形參繫結及模型資料輸出時使用註解驅動了。 <mvc:annotation-driven/> 預設建立的 ConversionService 例項即為 FormattingConversionServiceFactroyBean。

日期格式化

@DateTimeFormat 註解可對 java.util.Date、java.util.Calendar、java.long.Long 時間型別進行標註

  • pattern 屬性:型別為字串。指定解析/格式化欄位資料的模式,如:yyyy-MM-dd hh:mm:ss
  • iso 屬性
  • style 屬性

數值格式化

@NumberFormat 可對類似數字型別的屬性進行標註,它擁有兩個互斥的屬性:

  • style:型別為 NumberFormat.Style。用於指定樣式型別,包括 3 種:Style.NUMBER(正常數字型別)、 Style.CURRENCY(貨幣型別)、 Style.PERCENT(百分數型別)
  • pattern:型別為 String,自定義樣式,如 pattern="#,###"

資料校驗

JSR 303 引入

  • 只做前端校驗是不安全的;在重要資料一定要加上後端驗證。SpringMVC:可以 JSR 303 來做資料校驗
  • JSR 303 是 Java 為 Bean 資料合法性校驗提供的標準框架,它已經包含在 JavaEE 6.0 中
    • JDBC 規範 → 實現(各個廠商的驅動包)
    • JSR 303 規範 → Hibernate Validator(第三方校驗框架)
  • JSR 303 通過在 Bean 屬性上標註類似於 @NotNull、@Max 等標準的註解指定校驗規則,並通過標準的驗證介面對 Bean 進行驗證
  • Hibernate Validator 是 JSR 303 的一個參考實現,除支援所有標準的校驗註解外,它還支援以下的擴充套件註解

實現步驟

導包&屬性加註解

  • 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 了)

@Valid&BindingResult

  • 在已經標註了 JSR303 註解的表單/命令物件前標註一個 @Valid,Spring MVC 框架在將請求引數繫結到該形參物件後,就會呼叫校驗框架根據註解宣告的校驗規則實施校驗
  • Spring MVC 是通過對處理方法簽名的規約來儲存校驗結果的:前一個表單/命令物件的校驗結果儲存到隨後的形參中,這個儲存校驗結果的形參必須是 BindingResult 或 Errors 型別,這兩個類都位於 org.springframework.validation 包中 // BindingResult 擴充套件了 Errors 介面
  • 需校驗的 Bean 物件和其繫結結果物件或錯誤物件時成對出現的,它們之間不允許宣告其他的形參。
  • 在目標方法中獲取校驗結果
    • 在表單/命令物件類的屬性中標註校驗註解,在處理方法對應的入參前新增 @Valid,Spring MVC 就會實施校驗並將校驗結果儲存在被校驗入參物件之後的 BindingResult 或 Errors 入參中。
    • 常用方法
      FieldError getFieldError(String field)
      List<FieldError> getFieldErrors()
      Object getFieldValue(String field)
      Int getErrorCount()
      

在頁面上顯示錯誤

  • Spring MVC 除了會將表單/命令物件的校驗結果儲存到對應的 BindingResult 或 Errors 物件中外,還會將所有校驗結果儲存到 “隱含模型”。
  • 即使處理方法的簽名中沒有對應於表單/命令物件的結果入參,校驗結果也會儲存在 “隱含物件” 中。
  • 隱含模型中的所有資料最終將通過 HttpServletRequest 的屬性列表暴露給 JSP 檢視物件,因此在 JSP 中可以獲取錯誤資訊
  • 在 JSP 頁面上可通過 <form:errors path="userName"> 顯示錯誤訊息;或者自己封裝好帶過去

效果展示:

提示訊息的國際化

  • 每個屬性在資料繫結和資料校驗發生錯誤時,都會生成一個對應的 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>