SpringMVC——自定义类型转换器、<mvc:annotation-driven/>解析、数据格式化和数据校验
一、数据绑定流程原理
原理:ssm187集
- Spring MVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象
- DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换、数据格式化工作 。将 Servlet 中的请求信息填充到入参对象中
- 调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingData 对象
- Spring MVC 抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
- Spring MVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是 DataBinder ,运行机制如下:
二、自定义类型转换器
2.1 类型转换器概述
- ConversionService 是 Spring 类型转换体系的核心接口。
- 可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC 容器中定义一个
ConversionService. Spring 将自动识别出 IOC 容器中的 ConversionService,并在 Bean 属性配置及 Spring MVC 处理方法入参绑定等场合使用它进行数据的转换 - 可通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器
- 例如:
2.2 Spring支持的转换器类型
Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactoryBean 中:
- Converter<S,T>(重点):将 S 类型对象转为 T 类型对象
- ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
- GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
2.3 自定义转换器示例
-
实现Converter接口,写一个自定义的类型转换器;
/** * 两个泛型 * S:Source * T:Target * 将s转为t */ public class MyStringToEmployeeConverter implements Converter<String, Employee> { @Autowired DepartmentDao departmentDao; /** * 自定义转换规则 * 目标将 [email protected] 转换成Employee对象 */ @Override public Employee convert(String source) { System.out.println("页面提交的将要转换的字符串:"+source); [email protected] Employee employee = new Employee(); if(source.contains("-")){ String[] split = source.split("-"); employee.setLastName(split[0]); employee.setEmail(split[1]); employee.setGender(Integer.parseInt(split[2])); employee.setDepartment(departmentDao.getDepartment(Integer.parseInt(split[3]))); System.out.println("转换之后的employee对象是:"+employee); } return employee; } }
-
将这个Converter配置在ConversionService中
Converter是ConversionService中的组件;
1、你的Converter得放进ConversionService 中;
2、将WebDataBinder中的ConversionService设置成我们这个加了自定义类型转换器的ConversionService;<!-- 告诉SpringMVC别用默认的ConversionService, 而用我自定义的ConversionService、可能有我们自定义的Converter; --> <!-- 以后写自定义类型转换器的时候,就使用FormattingConversionServiceFactoryBean来注册;既具有类型转换还有格式化功能 --> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <!--converters转换器中添加我们自定义的类型转换器 --> <property name="converters"> <set> <bean class="com.zb.component.MyStringToEmployeeConverter"></bean> </set> </property> </bean>
-
告诉SpringNMVC使用这个ConversionService
<!-- conversion-service="conversionService":告诉SpringMVC使用我们自己配置的类型转换组件 --> <mvc:annotation-driven conversion-service="conversionService"> </mvc:annotation-driven>
debug分析:
自定义类型转换器之前:
自定义类型转换器之后:源码上WebDataBinder上的ConversionService组件就替换了;
三、<mvc:annotation-driven/>解析(Spring4源码为例)
ssm190集视频(以下待补带理解)
- <mvc:annotation-driven /> 会自动注册RequestMappingHandlerMapping 、RequestMappingHandlerAdapter 与 ExceptionHandlerExceptionResolver 三个bean。
- 还将提供以下支持:
- 支持使用 ConversionService 实例对表单参数进行类型转换
- 支持使用 @NumberFormat annotation、@DateTimeFormat 注解完成数据类型的格式化
- 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
- 支持使用 @RequestBody 和 @ResponseBody 注解
分析关于配置mvc:default-servlet-handler和mvc:annotation-driven
- 既没有配置 <mvc:default-servlet-handler/> 也没有配置 <mvc:annotation-driven/>
动态资源能访问(@RequestMapping映射的资源),静态资源不能访问(.html , .js , .img等)
HandlerMapping:
- 动态能访问:
DefaultAnnotationHandlerMapping中的handlerMap中保存了每一个资源的映射信息;- 静态不能访问:
就是handlerMap中没有保存静态资源映射的请求,
HandlerAdapter:方法执行的适配器:
AnnotationMethodHandlerAdapter:帮我们执行目标方法;过时的;
- 配置了 <mvc:default-servlet-handler/> 但没有配置 <mvc:annotation-driven/>
静态资源可访问,动态资源不能访问
HandlerMapping中变化:
- 动态不能访问:DefaultAnnotationHandlerMapping没有了;用SimpleUrlHandlerMapping替换了,他的作用就是将所有请求直接交给tomcat;
- 静态能访问的原因:SimpleUrlHandlerMapping把所有请求都映射给tomcat
HandlerAdapter:
AnnotationMethodHandlerAdapter没有了
- 既配置了 <mvc:default-servlet-handler/> 又配置 <mvc:annotation-driven/>(重要)
HandlerMapping变化:
RequestMappingHandlerMapping:动态资源可以访问
handleMethods属性保存了每一个请求用哪个方法来处理;
SimpleUrlHandlerMapping:将请求直接交给tomcat;有他,静态资源就没问题;
HandlerAdapter:
原来的AnnotationMethodHandlerAdapter;被换成RequestMappingHandlerAdapter;
四、数据格式化
4.1 数据格式化概述
*对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。
- Spring 在格式化模块中定义了一个实现 ConversionService 接口的
FormattingConversionService
实现类,该实现类扩展了 GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能 - FormattingConversionService 拥有一个
FormattingConversionServiceFactroyBean
工厂类,后者用于在 Spring 上下文中构造前者,FormattingConversionServiceFactroyBean 内部已经注册了 :- NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用
@NumberFormat
注解 - JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用
@DateTimeFormat
注解
- NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用
- 装配了 FormattingConversionServiceFactroyBean 后,就可以在 Spring MVC 入参绑定及模型数据输出时使用注解驱动了。
- <mvc:annotation-driven/> 默认创建的 ConversionService 实例即为
DefaultFormattingConversionService
- <mvc:annotation-driven/> 默认创建的 ConversionService 实例即为
4.2 日期格式化
@DateTimeFormat
注解可对 java.util.Date、java.util.Calendar、java.long.Long 时间类型进行标注:pattern 属性
:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss
”- iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) – 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、 ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
- style 属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式
代码示例:将表单中输入的字符串解析成格式化字段数据
前台表单项:
birth:<form:input path="birth"/><br/><!-- 输入类型为2021-3-26 -->
在Employee类增加日期对象属性并标注@DateTimeFormat注解:
//规定页面提交的日期格式
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
注意:如果使用@DateTimeFormat注解标注,则框架默认支持的格式为斜线方式。1990/09/09
4.3 数值格式化
@NumberFormat
可对类似数字类型的属性进行标注,它拥有两个互斥的属性:- style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、 Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
pattern
:类型为 String,自定义样式,如pattern="#,###
";
实例代码:
Salary : <form:input path="salary"/>
@NumberFormat(pattern="#,###,###.#")
private double salary ;
五、数据校验(重点)
对于一些很重要的数据,如果只是做前端校验是不安全的,必须要加上后端的校验。
在后端的校验方法:
-
可以写程序将每一个数据取出进行校验,如果失败直接来到添加页面,提示重新填写。(不推荐)
-
SpringMVC:可以JSR-303(Java Specification Requests的第303条规范)来做数据校验
JSR303:规范 ------- 实现:Hibernate Validator(第三方校验框架)
5.1 JSR 303
- JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
- JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
5.2 Hibernate Validator
Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
5.3 Spring MVC 数据校验
-
Spring MVC 数据校验
- Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。
- Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验
- Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个 LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中。
- Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下。
- <mvc:annotation-driven/> 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @valid 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作
- 在已经标注了 JSR303 注解的表单/命令对象前标注一个 @Valid,Spring MVC 框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验
- Spring MVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是 BindingResult 或 Errors 类型,这两个类都位于 org.springframework.validation 包中
- 需校验的 Bean 对象和其绑定结果对象或错误对象时成对出现的,它们之间不允许声明其他的入参
- Errors 接口提供了获取错误信息的方法,如 getErrorCount() 或 getFieldErrors(String field)
- BindingResult 扩展了 Errors 接口
-
在目标方法中获取校验结果
在表单/命令对象类的属性中标注校验注解,在处理方法对应的入参前添加 @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”> 显示错误消息
5.4 代码示例
-
导入校验框架的jar包
hibernate-validator-5.0.0.CR2\dist
- hibernate-validator-5.0.0.CR2.jar
- hibernate-validator-annotation-processor-5.0.0.CR2.jar
hibernate-validator-5.0.0.CR2\dist\lib\required (EL就不需要加了)
- classmate-0.8.0.jar
- jboss-logging-3.1.1.GA.jar
- validation-api-1.1.0.CR1.jar
注意:有几个带el的jar不导入;tomcat中有;如果tomcat的版本是7.0以下;tomcat7.0以上el表达式比较强大;如果是7.0以下将带el的几个jar放在tomcat的lib文件夹下;
-
在验证属性上增加验证注解
public class Employee { private Integer id; @NotEmpty @Length(min=6,max=18) private String lastName; @Email private String email; //规定页面提交的日期格式 @DateTimeFormat(pattern="yyyy-MM-dd") @Past//表示只能是过去的某个日期 private Date birth; }
-
在SpringMVC封装对象的时候,告诉SpringMVC这个javaBean需要校验
控制器方法:
/** * 保存员工信息 * @param employee * @return */ @RequestMapping(value="/emp",method=RequestMethod.POST) //增加@Valid注解,验证失败会报错。 public String addEmp(@Valid Employee employee){ System.out.println("新增的Employee信息"+employee); employeeDao.save(employee); //重定向到查询所有员工的请求 return "redirect:/emps"; }
-
获取校验结果:给需要校验的javaBean后面紧跟一个BindingResult。这个BindingResult就是封装前一个bean的校验结果
/** * 保存员工信息 * @param employee * @return */ @RequestMapping(value="/emp",method=RequestMethod.POST) //增加@Valid注解。如果没有BindingResult,验证失败会报400 public String addEmp(@Valid Employee employee,BindingResult result,Model model){ System.out.println("新增的Employee信息"+employee); System.out.println("校验结果为:"+result); System.out.println("*************************************"); //获取是否有校验错误 boolean hasErrors = result.hasErrors(); if(hasErrors){ List<FieldError> errors = result.getFieldErrors(); Map<String,Object> errorsMap = new HashMap<>(); for(FieldError fieldError : errors){ System.out.println("错误消息提示:"+fieldError.getDefaultMessage()); System.out.println("错误的字段是:"+fieldError.getField()); errorsMap.put(fieldError.getField(), fieldError.getDefaultMessage()); } //将错误信息保存到请求域中 model.addAttribute("errorInfo", errorsMap); return "add"; }else{ employeeDao.save(employee); //重定向到查询所有员工的请求 return "redirect:/emps"; } }
-
前台打印错误消息
方法一:使用Spring的表单标签(form:errors取出错误信息)
<form:form action="${ctp }/emp" method="post" modelAttribute="employee"> lastName:<form:input path="lastName"/><form:errors path="lastName"></form:errors> <br/> email:<form:input path="email"/><form:errors path="email"></form:errors> <br/> birth:<form:input path="birth"/><form:errors path="birth"></form:errors> <br/> </form:form>
方法二:将错误信息放到请求域中
<form:form action="${ctp }/emp" method="post" modelAttribute="employee"> lastName:<form:input path="lastName"/>${errorInfo.lastName }<br/> email:<form:input path="email"/>${errorInfo.email }<br/> birth:<form:input path="birth"/>${errorInfo.birth }<br/> </form:form>
5.5 提示消息的国际化
每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的 FieldError 对象。
当一个属性校验失败后,校验框架会为该属性生成 4 个消息代码,这些代码以校验注解类名为
前缀
,结合 modleAttribute、属性名及属性类型名
生成多个对应的消息代码:例如 Employee类中的 email属性标准了一个 @email注解,当该属性值不满足 @Pattern 所定义的规则时, 就会产生以下 4 个错误代码:
- Email.employee.email; 校验规则.隐含模型中这个对象的key.对象的属性。
表示如果是隐含模型中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 在调用处理方法时发生了错误
注册国际化资源文件
5.6 代码示例
1、先编写国际化配置文件
-
errors_en_US.properties
#精确优先 #Email.email=email incorrect!~~ Email=email buzhengque~~~ NotEmpty=must not empty~~ #占位符 动态传入 Length.java.lang.String=length incorrect ,must between { 2} and { 1} ~~ Past=must a past time~~~ typeMismatch.birth=birth geshi buzhengque
动态传入消息参数
{0}:永远都是当前属性名;
{1},{2}:参数的字典序。如下所示 {1} 代码的是 max 的值,{2} 代表的是 min 的值 -
errors_zh_CN.properties
Email.email=邮箱不对!~~ NotEmpty=不能为空~~ Length.java.lang.String=长度不对~~ Past=时间必须是过去的~~~ typeMismatch.birth=生日的格式不正确
2、让SpringMVC管理国际化资源文件;
<!-- 管理国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<!-- basename指定基础名 -->
<property name="basename" value="errors"></property>
</bean>
3、add.jsp页面获取错误信息
lastName:<form:input path="lastName"/><form:errors path="lastName"></form:errors><br/>
email:<form:input path="email"/><form:errors path="email"></form:errors><br/>
birth:<form:input path="birth"/><form:errors path="birth"></form:errors><br/>
注意:如果注解已经有 message 属性
了,那么这个字段将会失去国际化功能,每次输出的都是这个message信息
@NotEmpty(message="不能为空")
@Length(min=6,max=18,message="长度")
private String lastName;