SpringMVC源码分析之数据绑定与国际化

1. 数据绑定

SpringMVC的数据绑定是何时发生的呢?我们知道DispatcherServlet去调用handler(处理器)是通过HandlerAdapter:mv = ha.handle(processedRequest, response, mappedHandler.getHandler());,通过Debug发现数据绑定流程如下:
resolveArgument

getMethodArgumentValues()方法获取调用的处理器方法的参数,通过参数解析器argumentResolvers逐一解析方法的参数,ModelAttributeMethodProcessor的resolveArgument()方法部分源码如下:

//将WebReqeust、方法参数实例及参数名传递给WebDataBinderFactory实例创建数据绑定器WebDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
    if (!mavContainer.isBindingDisabled(name)) {
        //通过binder绑定请求参数
        bindRequestParameters(binder, webRequest);
    }
    //通过binder校验数据
    validateIfApplicable(binder, parameter);
    if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
        throw new BindException(binder.getBindingResult());
    }
}

binder变量的数据如下:
binder

数据绑定流程:

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

1.1 数据转换

Spring MVC 上下文中内建了很多转换器,可完成大多数 Java 类型的转换工作。ConversionService 是 Spring 类型转换体系的核心接口,ConversionService的converters属性是所有注册的类型转换器的集合,使用这些类型转换器实现数据的转换,默认的ConversionService是DefaultFormattingConversionService同时支持数据的格式化和转换。

1.1.1 自定义类型转换器

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

  • Converter<S,T>:将S类型对象转为T类型对象
  • ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
  • GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换

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

示例如下:

/**
 * 将String转换为Employee
 * String的格式为"name=value,name=value"
 * @author yutao
 * @create 2018-06-23 15:42
 */
public class EmployeeConverter implements Converter<String,Employee> {
    /**
     * @param s
     * @return employee
     */
    @Override
    public Employee convert(String s) {
        String[] strs = s.split(",");
        Employee employee = new Employee();
        Class clazz = employee.getClass();
        for (int i=0;i<strs.length;i++) {
            String name = strs[i].substring(0, strs[i].indexOf("="));
            String value = strs[i].substring(strs[i].indexOf("=")+1);
            String methodName = "get"+name.substring(0,1).toUpperCase()+name.substring(1);
            try {
                Method method = clazz.getMethod(methodName);
                method.invoke(employee,value);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        return employee;
    }
}

SpringMVC配置文件注册EmployeeConverter

<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
     <property name="converters">
         <list>
             <bean class="com.controller.converter.EmployeeConverter"></bean>
         </list>
     </property>
 </bean>

1.1.2 mvc:annotation-driven

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

  • 支持使用 ConversionService 实例对表单参数进行类型转换(如果不使用该注解SpringMVC依然可以支持一些基本类型的转换)
  • 默认创建FormattingConversionServiceFactroyBean实例,支持@NumberFormat 、@DateTimeFormat注解完成数据类型的格式化
  • 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
  • 支持使用 @RequestBody 和 @ResponseBody 注解

<mvc:annotation-driven>正常情况下都会开启。

1.1.3 @InitBinder

@InitBinder 标识的方法,可以对 WebDataBinder 对象进行初始化,WebDataBinder 是 DataBinder 的子类,用于完成由表单字段到 JavaBean 属性的绑定,@InitBinder方法不能有返回值,它必须声明为void,@InitBinder方法的参数通常是 WebDataBinder。

@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
    //该字段不允许被绑定
    webDataBinder.setDisallowedFields("id");
}

1.2 数据格式化

前面我们提到FormattingConversionService既具有类型转换的功能,又具有格式化的功能,FormattingConversionService 拥有一个FormattingConversionServiceFactroyBean 工厂类,后者用于在 Spring 上下文中构造前者。开启<mvc:annotation-driven>默认创建FormattingConversionServiceFactroyBean实例,因此支持@NumberFormat和@DateTimeFormat注解。

1.2.1 @NumberFormat

@NumberFormat 可对类似数字类型的属性进行标注:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface NumberFormat {
    /**
     * 与Style是互斥属性,默认即为Style.DEFAULT
     */
    Style style() default Style.DEFAULT;
    /**
     * 自定义样式
     */
    String pattern() default "";
    enum Style {

        /**
         * The default format for the annotated type: typically 'number' but possibly
         * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount)}.
         * @since 4.2
         */
        DEFAULT,

        /**
         * 正常数字类型
         */
        NUMBER,

        /**
         * 百分数类型
         */
        PERCENT,

        /**
         * 货币类型
         */
        CURRENCY
    }
}

1.2.2 @DateTimeFormat

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DateTimeFormat {
    /**
     * 字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期格式,第二位表示时间的格式:
     * S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、
     * F:完整日期/时间格式、-:忽略日期或时间格式
     */
    String style() default "SS";
    /**
     * 自己指定解析/格式化字段数据的模式
     */
    String pattern() default "";
    /**
     * Common ISO date time format patterns.
     */
    enum ISO {
        /**
         * The most common ISO Date Format {@code yyyy-MM-dd},
         * e.g. "2000-10-31".
         */
        DATE,

        /**
         * The most common ISO Time Format {@code HH:mm:ss.SSSZ},
         * e.g. "01:30:00.000-05:00".
         */
        TIME,

        /**
         * The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSZ},
         * e.g. "2000-10-31T01:30:00.000-05:00".
         * <p>This is the default if no annotation value is specified.
         */
        DATE_TIME,

        /**
         * Indicates that no ISO-based format pattern should be applied.
         */
        NONE
    }
}

1.3 数据校验

JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中,JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对 Bean进行验证。Hibernate Validator 是 JSR 303 的一个参考实现。
SpringMVC 4.0拥有自己独立的数据校验框架,同时支持 JSR303 标准的校验框架,<mvc:annotation-driven/> 会默认装配好一个LocalValidatorFactoryBean,LocalValidatorFactroyBean 既实现了 Spring 的Validator 接口,也实现了 JSR 303 的 Validator 接口,但是Spring本事并没有提供JSR303的实现,还是需要依赖JSR303的实现者jar包。

JSR303常用注解:@Null、@NotNull、@AssertTrue、AssertFlase、@Min(value)、@Max(value)、@DecimalMin(value)、@DecimalMax(value)、@Size(max,min)、@Digits(integer,fraction)、@Past、@Future、@Pattern(value)
Spring校验常用注解:@Email、@Length、@NotEmpty、@Range

在已经标注了 JSR303 注解/Spring校验注解的表单/命令对象前标注一个@Valid,Spring MVC 框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验。Spring MVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是 BindingResult 或Errors 类型。

1.3.1 页面显示校验错误信息

Spring MVC 除了会将表单/命令对象的校验结果保存到对应的 BindingResult 或 Errors 对象中外,还会将所有校验结果保存到 “隐含模型”,隐含模型中的所有数据最终将通过 HttpServletRequest 的属性列表暴露给 JSP 视图对象,在JSP 页面上可通过 <form:errors path="name">显示错误消息,如果使用其他视图技术只有支持jstl技术才可,示例见后续国际化与错误信息显示

2. 国际化

2.1 LocaleResolver

LocaleResolver

接口和类说明
LocaleResolver本地化解析器父接口,提供了resolveLocale和setLocale二个接口
LocaleContextResolverLocaleResolver的子接口,增加了resolveLocaleContext和setLocaleContext二个接口
AcceptHeaderLocaleResolver直接实现LocaleResolver接口,可以设置defaultLocale和supportedLocales,从请求头Accept-Language中获取Locale
AbstractLocaleResolverLocaleResolver的抽象类,增加了defalutLocale属性
AbstractLocaleContextResolver继承AbstractLocaleResolver同时实现LocaleContextResolver接口的抽象类,实现resolveLocale和setLocale方法,实际上各自调用由其子类实现的resolveLocaleContext和setLocaleContext,并增加了defaultTimeZone属性
SessionLocaleResolver重写了resolveLocale和resolveLocaleContext,将locale和timeZone都保存到session中,resolveLocale将从session中取locale
FixedLocaleResolver重写了resolveLocale和resolveLocaleContext,都会去获取默认的defaultLocale,如果为空则使用Locale.getDefault去获取

2.2 Local的获取

SpringMVC的国际化处理如下:

  • 默认情况下,SpringMVC 根据 Accept-Language 参数判断客户端的本地化类型
  • 当接受到请求时,SpringMVC 会在上下文中查找一个本地化解析器(LocalResolver),找到后使用它获取请求所对应的本地化类型信息
  • SpringMVC 还允许装配一个动态更改本地化类型的拦截器,这样通过指定一个请求参数(locale)就可以控制单个请求的本地化类型

DispatcherServlet中关于国际化的源码如下:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 通过本地化解析器解析获取Locale
    Locale locale = this.localeResolver.resolveLocale(request);
    response.setLocale(locale);

    //后面代码省略......
}

SpringMVC默认的本地化解析器是AcceptHeaderLocaleResolver,该解析器会根据请求头的“Accept-Language”参数判断客户端本地化类型,源码如下:

@Override
public Locale resolveLocale(HttpServletRequest request) {
    //获取默认的Locale
    Locale defaultLocale = getDefaultLocale();
    //如果有默认Locale不为空且请求头参数“Accept-Language”为空则使用默认
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    }
    //获取请求头参数“Accept-Language”的Locale
    Locale requestLocale = request.getLocale();
    //supportedLocales为空或包含requestLocale则为true
    if (isSupportedLocale(requestLocale)) {
        return requestLocale;
    }
    //获取request.getLocales(存在多个Locale)中被supportedLocales包含的Locale
    Locale supportedLocale = findSupportedLocale(request);
    if (supportedLocale != null) {
        return supportedLocale;
    }
    return (defaultLocale != null ? defaultLocale : requestLocale);
}

上述实现国际化的方式是通过客户端切换本地的语言类型来实现的。如何动态的实现语言的切换呢?(如通过超链接更改这个网站的语言类型)SpringMVC提供了LocaleChangeInterceptor拦截器默认拦截处理带有locale参数的请求,再将locale存入session中即可对整个网站实现国际化。大致流程如下:

国际化

LocaleChangeInterceptor#perHandler源码如下:

/**
 * DispatcherServlet的doDispatch()方法调用
 * mappedHandler.applyPreHandle(processedRequest, response)
 * 执行每个拦截器的preHandle方法
 */
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws ServletException {
    //getParamName()默认值为locale
    String newLocale = request.getParameter(getParamName());
    //如果locale参数不为空
    if (newLocale != null) {
        //检查HttpMethod请求
        if (checkHttpMethod(request.getMethod())) {
            //获取注册的本地化解析器
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException(
                        "No LocaleResolver found: not in a DispatcherServlet request?");
            }
            try {
                //设置本地化解析器的Locale为当前请求参数locale
                localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
            }
            catch (IllegalArgumentException ex) {
                if (isIgnoreInvalidLocale()) {
                    logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
                }
                else {
                    throw ex;
                }
            }
        }
    }
    // Proceed in any case.
    return true;
}

SessionLocaleResolver的setLocale()方法由其父类AbstractLocaleContextResolver实现,setLocale()又去调用由其子类实现的setLocaleContext()方法,源码如下:

/**
 * AbstractLocaleContextResolver中实现
 */
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
    setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}

/**
 * SessionLocaleResolver中实现
 */
@Override
public void setLocaleContext(HttpServletRequest request, HttpServletResponse response, LocaleContext localeContext) {
    Locale locale = null;
    TimeZone timeZone = null;
    if (localeContext != null) {
        locale = localeContext.getLocale();
        if (localeContext instanceof TimeZoneAwareLocaleContext) {
            timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
        }
    }
    //将locale和timeZone设置为session的属性
    WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);
    WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
}

SessionLocaleResolver的resolveLocale()方法源码如下:

@Override
public Locale resolveLocale(HttpServletRequest request) {
    //从session中获取locale
    Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);
    if (locale == null) {
        //如果请求的locale为空,则使用默认的locale
        locale = determineDefaultLocale(request);
    }
    return locale;
}

protected Locale determineDefaultLocale(HttpServletRequest request) {
    //获取defaultLocale
    Locale defaultLocale = getDefaultLocale();
    if (defaultLocale == null) {
        //默认defaultLocale为空,则获取请求头的Accept-Language
        defaultLocale = request.getLocale();
    }
    return defaultLocale;
}

由此可见,SessionLocaleResolver会将request请求参数locale作为本地化Locale,并放入session中,如果locale为空使用默认值defaultLocale,依然为空则使用客户端请求头参数”Accept-Language”值作为Locale。

SpringMVC配置文件如下:

   <!-- 配置国际化资源文件 -->
   <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
       <!--classpath下资源文件基础名-->
       <property name="basename" value="i18n"></property>
   </bean>

   <!-- 本地化解析器 -->
   <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
       <!--设置默认locale-->
       <property name="defaultLocale" value="zh_CN"/>
   </bean>

   <mvc:interceptors>
       <!-- 国际化拦截器,拦截请求参数locale -->
       <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
   </mvc:interceptors>

2.3 国际化与错误信息显示

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

error.jsp:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>error</title>
</head>
<body>
<form:form action="/dataValidate" modelAttribute="department">
    <form:input path="departmentName"/>
    <form:errors path="departmentName"></form:errors>
    <br/>
    <input type="submit" value="Submit"/>
</form:form>
</body>
</html>

Department.java

public class Department {
    private Integer id;
    @Email
    private String departmentName;
    //...
}

处理器:

/**
 * 查看FieldError的toString的打印信息可以发现
 * codes [Email.department.departmentName,Email.departmentName,Email.java.lang.String,Email]
 * default message [not a well-formed email address]
 * 在国际化资源文件中便是使用Email.department.departmentName作为key,
 * 当没有配置时使用default message中内容作为提示信息
 */
@RequestMapping("/dataValidate")
public String localeMessage(@Valid Department department, BindingResult bindingResult,Model model) {
    model.addAttribute("department",department);
    if (bindingResult.hasErrors()) {
        //获取所有的错误字段的信息
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        for (FieldError fieldError : fieldErrors) {
            System.out.println(fieldError.toString());
            System.out.println(fieldError.getField() + ":" + fieldError.getCode()+fieldError.getDefaultMessage());
        }
        return "error";
    }
    return "success";
}

i18n_zh_CN.properties

#department的deparmentName属性是否是Email
Email.department.departmentName=邮件地址输入不合法

i18n_en_US.properties

Email.department.departmentName=Not a legitimate email address
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值