SpringMVC:转换器、格式化器、数据校验
作者:互联网
1,数据类型转化与绑定
数据绑定可以将用户输入动态地绑定到应用程序的领域对象(或任何处理用户输入的对象),Spring使用DataBinder执行数据绑定,使用Validator执行数据校验,它们共同组成了validation包,validation包主要适用于SpringMVC框架,也可脱离SpringMVC使用。
SpringMVC执行数据绑定的核心组件是DataBinder对象,整个数据转换、绑定的校验的大致过程如下:
- SpringMVC将ServletRequest对象及目标处理方法的形参传给WebDataBinderFactory对象,WebDataBinderFactory会为之创建对应的DataBuilder的对象。
- DataBuilder会调用Spring容器中的ConversionService Bean对ServletRequest的请求参数执行数据类型转换等操作,然后用转换结果填充处理方法的参数。
- 使用Validator组件对处理方法的参数执行数据校验,如果存在校验错误,则生成对应的错误对象。
- SpringMVC负责提取BindingResult中的参数与错误对象,将它们保存到处理方法的model属性中,以便控制器中的处理方法访问这些信息。
DataBinder的大致流程如图:
1.1,BeanWrapper简介
BeanWrapper是DataBinder的基础,BeanWrapper接口封装了对Bean的基本操作,包括读取和设置Bean的属性值。一般来说,在应用程序中不需要直接使用BeanWrapper,只要使用DataBinder即可;而DataBinder则借助BeanWrapper的支持,可以动态地将字符串转换成目标对象,为Bean的属性赋值。
BeanWrapper接口提供了一个BeanWrapper实现类,程序创建BeanWrapperImpl对象时,必须传入被包装的对象——该对象必须是一个符合JavaBean规范的对象(JavaBean规范要求该Java类必须提供无参的构造器,而且为每个需要暴露的属性都提供对应的setter和getter方法)。
程序使用BeanWrapperImpl包装Java对象之后,接下来通过BeanWrapper来设置和访问Bean属性时,只需要传入字符串属性,无须理会属性的类型——因为Spring内置的各种PropertyEditor会自动完成类型转换。当然如果希望BeanWrapper能自动处用户自定义类型,则需要开发自定义的PropertyEditor。
BeanWrapper可操作如下形式属性:
- name:如果获取name属性,则对应于调用getName()或isName()方法;如果对name属性,则对应于调用setName()方法。
- author.name:如果获取author.name属性,则对应于调用getAuthor().getName()或getAuthor().isName()方法;如果对author.name属性赋值,则对应于调用getAuthor().setName()方法。
- books[2]:表示访问books属性的第三个元素。books属性的值可以是List集合、数组或其他支持自然排序的集合。
- scores['java']:表示访问scores属性的key为'java'的value,scores属性必须是Map类型。
public class BeanWrapperTest { public static void main(String[] args) { Book book = new Book(); BeanWrapperImpl bookWrapper = new BeanWrapperImpl(book); //将book对象包装成BeanWrapper实例 bookWrapper.setPropertyValue("name","盗墓笔记"); //通过BeanWrapper为name属性设置值 System.out.println("当前name属性值:"+book.getName()); System.out.println("当前name属性值:"+bookWrapper.getPropertyValue("name")); //---------------------------------------- PropertyValue v = new PropertyValue("name", "盗墓笔记十年"); bookWrapper.setPropertyValue(v); System.out.println("当前name属性值:"+book.getName()); System.out.println("当前name属性值:"+bookWrapper.getPropertyValue("name")); //---------------------------------------- Author author = new Author(); //将author包装成BeanWrapper实例 BeanWrapperImpl authorWrapper = new BeanWrapperImpl(author); authorWrapper.setPropertyValue("name","南派三叔"); bookWrapper.setPropertyValue("author",author); System.out.println("作者名:"+bookWrapper.getPropertyValue("author.name")); bookWrapper.setPropertyValue("author.age",25); System.out.println("作者年龄:"+authorWrapper.getPropertyValue("age")); } } =================================================== 当前name属性值:盗墓笔记 当前name属性值:盗墓笔记 当前name属性值:盗墓笔记十年 当前name属性值:盗墓笔记十年 作者名:南派三叔 作者年龄:25
1.2,PropertyEditor与内置实现类
正如前面所看到的,程序调用BeanWrapper的setPropertyValue方法时,所有传入参数的类型都是String(从XML配置文件中解析得到的值都是String类型的,通过ServletRequest获取的请求参数也都是String类型的),而BeanWrapper则可以将String类型自动转换成目标类型。
Spring底层由PropertyEditor负责完成类型转换的,Spring内置了多种PropertyEditor,它们都是java.beans.PropertyEditorSupport(PropertyEditor的实现类)的子类,可用于完成各种常用类型的转换:
- ByteArrayPropertyEditor:字节数组的PropertyEditor的实现类)的子类,能将字符串转换成对应的字节数组。BeanWrapperImpl默认注册。
- ClassEditor:类PropertyEditor,能将类名字符串转换成对应的类。如果该类不存在,则抛出IllegalArgumentException异常。BeanWrapperImpl默认注册。
- CustomBooleanEditor:Boolean属性的自定义PropertyEditor。BeanWrapperImpl默认注册,用户也可以覆盖该注册。
- CustomCollectionEditor:集合的PropertyEditor,能将任何资源集合转换成目标集合类型。
- CustomDateEditor:Date的自定义PropertyEditor,可以自己定义日期格式。BeanWrapperImpl默认没有注册,如果需要使用该PropertyEditor,用户必须提供合适的日期格式,然后手动注册。
- CustomNumberEditor:Number类如Integer、Long、Float、Double的自定义PropertyEditor。BeanWrapperImpl默认注册,用户也可以覆盖该注册。
- FileEditor:File类的PropertyEditor,能将字符串转换成java.io.File对象。BeanWrapperImpl默认注册。
- InputStreamEditor:单向PropertyEditor,能读取一个文本字符串,然后通过内部的PropertyEditor和Resource,创建对应的InputStream。因此,InputStream属性可直接使用字符串设置。注意:系统并没有关闭InputStream,使用完后,记得关闭流。BeanWrapperImpl默认注册。
- LocaleEditor:Locale类的PropertyEditor,能将字符串转换成Locale对象,反之亦然。字符串必须遵守[language]_[country]_[variant]格式,BeanWrapperImpl默认注册。
- PatternEditor:Pattern类对应的PropertyEditor,能将字符串转换成Pattern对象,反之亦然。
- PropertiesEditor:能将字符串转换成Properties对象,字符串格式必须符合Javadoc中描述的java.lang.Properties的格式。BeanWrapperImpl默认注册。
- StringArrayPropertyEditor:能将用逗号分隔的字符串(字符串必须满足CSV格式)转换成字符串数组。BeanWrapperImpl默认注册。
- StringTrimmerEditor:去掉字符串空格的PropertyEditor,可选特性——能将字符串转换成null。BeanWrapperImpl默认没有注册,如需使用,用户手动注册。
- URLEditor:能将字符串转换成其对应的URL对象。BeanWrapperImpl默认注册。
1.3,自定义PropertyEditor
Spring无法预知用户自定义的类,因而无法将字符串转换成用户自定义的实例。如果要将字符串转换成用户自定义类的实例,则需要执行如下两步:
- 实现自定义的PropertyEditor类型,该类需要实现PropertyEditor接口,通常只要继承PropertyEditorSupport基类并重写它的setAsText()方法即可——因为该基类已经实现了PropertyEditor接口。
- 注册自定义的PropertyEditor类,通常有三种方法:
(1)默认注册:将PropertyEditor实现类与被转换类放在相同的包内,且让PropertyEditor实现类的类名为“<目标类>Editor”即可。比如需要转换的目标类是Author,则让PropertyEditor实现类的类名为AuthorEditor即可。
(2)使用@InitBinder修饰的方法完成注册。
(3)使用WebBindingInitializer注册全局PropertyEditor。
下面要完成转换Book类包含的4个属性,其中id、name较为简单,Spring可自行转换;而publishDate属性是Date类型,除非用户总能以Spring期望的格式输入,否则Spring通常无法自行转换;author属性是Author类型,Spring绝对无法转换。
public class Book { private Integer id; private String name; private Date published; private Author author; ... } -------------------------- public class Author { private Integer id; private String name; private int age; ... }
public class AuthorEditor extends PropertyEditorSupport { //重写setAsText方法,该方法将字符串转换成目标对象 public void setAsText(String text) throws IllegalArgumentException { //将字符串参数以“-”为分隔符,分割成字符串数组 String args[] = text.split("-"); Author author = new Author(Integer.parseInt(args[0]), args[1], Integer.parseInt(args[2])); //将创建的Author对象传给setValue()方法 setValue(author); } }
实现自定义的Author只要继承PropertyEditorSupport,并重写它的setAsText()方法即可。由于上面转换器的类名是AuthorEditor(为Author+Editor的组合),且该转换器类与Author放在相同的包内,因此Spring会自动注册该转换器。
public class DateEditor extends PropertyEditorSupport { public void setAsText(String text) throws IllegalArgumentException { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); try { Date date = dateFormat.parse(text); setValue(date); }catch (Exception e){ e.printStackTrace(); } } }
该转换器使用SimpleDateForm按“yyyy-MM-dd”模板执行转换,这意味着它要求用户在publishDate表单域中输入的日期要匹配“yyyy-MM-dd”格式。
由于DateEditor无法与Date类放在相同的包下,因此必须显示注册该类型转换器。
@ControllerAdvice public class ControllerAspect { @InitBinder public void initBinder(WebDataBinder binder){ //注册自定义PropertyEditor,指定Date类使用DateEditor进行类型转换 binder.registerCustomEditor(Date.class,new DateEditor()); } }
上面initBinder()方法使用了@InitBinder修饰,这意味着该方法会在控制器初始化的时候执行。该方法体内只有一行代码——程序调用WebDataBinder为Date类注册了自定义类型转换器DateEditor。经过上面步骤,两个类型转换器都已注册完成,其中AuthorEditor采用隐式注册,而DateEditor则采用@InitBinder方法显式注册。
@Controller public class BookController { @GetMapping("/{url}") public String url(@PathVariable String url) { return url; } // @PostMapping指定该方法处理/addBook请求 @PostMapping("/addBook") public String add(Book book, Model model) { System.out.println("添加的图书:" + book); model.addAttribute("tip", book.getName() + "图书添加成功!"); model.addAttribute("book", book); return "success"; } }
1.4,使用WebBindingInitializer注册全局PropertyEditor
如果希望咋全局范围内使用自定义的类型转换器,则可通过实现WebBindingInitializer接口并实现该接口中的initBinder()方法来注册自定义的类型转换器。与之前的区别在于注册DataEditor的方式有所改变。
public class DatebindingInitializer implements WebBindingInitializer { public void initBinder(WebDataBinder webDataBinder) { webDataBinder.registerCustomEditor(Date.class,new DateEditor()); } }
上面的类实现了WebBindingInitializer接口,并实现了接口中initBinder()方法——该方法用于注册全局的类型转化器。之后还需要配置HandlerAdapter来加载。
原本<mvc:annotation-driven/>元素会自动配置HandlerAdapter、HandlerMapping和HandlerExceptionResovler这些特殊的Bean,但由于<mvc:annotation-driven/>元素没有提供属性来加载WebBindingInitializer实现类,开发者必须手动配置HandlerAdapter、HandlerMapping和HandlerExceptionResolver。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" p:webBindingInitializer="#{new com.ysy.springmvc.binding.DatebindingInitializer()}"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver"/> </beans>
上面配置文件取消了<mvc:annotation-driven/>元素的使用,而是改为使用上面三个Bean配置来替代:
第一个Bean配置了HandlerAdapter特殊的Bean,并使用该特殊的Bean来加载自定义的DateBindingInitializer实现类;后面两个Bean分别配置了HandlerMapping、HandlerExceptionResolver两个特殊的Bean。
通过上面可知,如果要通过WebBindInitializer来注册全局的PropertyEditor,SpringMVC配置文件就不能利用简化的<mvc:annotation-driven/>元素。但通过这种方式注册的PropertyEditor可以作用于所有控制器,而不需要为每个控制器都单独注册局部的PropertyEditor,当程序需要为多个控制器注册公用的PropertyEditor时,这种方式可以提供更好的性能。
1.5,使用ConversionService执行转换
ConversionService是从Spring3开始引入的类型转换机制,相比传统的PropertyEditor类型转换机制,ConversionService具有以下优点:
- 可完成任意两个Java类型之间的转换,而不像PropertyEditor只能完成String与其他Java类型之间的转换。
- 可利用目标类型上下文信息(如注解),因此,ConversionService可支持基于注解的类型转换。
Spring同时支持传统的PropertyEditor类型转换机制和ConversionService转换。一般来说,如果只是为个别控制器提供局部的类型转换器,则依然可以使用传统的PropertyEditor类型转换机制;如果要注册全局的类型转换器,则建议使用ConversionService。
基于ConversionService提供自定义的类型转换器同样需要两步:
- 开发自定义的类型转换器。自定义的类型转换器可实现Converter、ConverterFactory和GenericConverter接口的其中之一。
- 使用ConversionService配置自定义的类型转换器。
Converter、ConverterFactory和GenericConverter这三个接口区别:
(1)Converter<S, T>:该接口是最简单的转换接口。该接口中只有一个方法:
T convert(S source)
该方法负责将S类型对象转换成T类型对象 。
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
(2)ConverterFactory<S, T>:如果希望将一种类型的对象转换成另一种类型及其子类对象,比如将Spring转换成Number及Integer、Double等对象,就需要一些列的Converter,如StringToInteger、StringToDouble等。ConverterFactory<S, R>接口的作用就是根据要转换的目标类型来提供实际的Converter对象。该接口中也只定义了一个方法:
<T extends R> Converter<S, T> getConverter(Class<T> targetType)
从方法定义可以看出,该方法就是根据目标类型targetType来返回对应的Converter对象的。其本身并能完成实际的类型转换,它只负责“生产”Converter工厂,它会根据要转换的目标类型“生产”实际的Converter。因此,它必须与多个Converter结合使用。
(3)GenericConverter:这是最灵活的,也是最复杂的类型转换器接口,它可以完成两种或两种以上类型之间转换。而且GenericConverter实现类可以访问源类型和目标类型的上下文,根据上下文信息进行转换。简单来说,它可以解析成员变量上的注解信息,并根据注解信息进行类型转换。GenericConverter接口中定义了如下两个方法:
Set<GenericConverter.ConvertiblePair> getConvertibleTypes()
该方法的返回值决定类转换器能对那些类型执行转换。其中ConvertiblePair集合元素封装了源类型和目标类型,该方法返回的Set包含几个元素,该转换器就支持几组源类型和目标类型之间的转换。
一般来说,使用简单的Converter和ConverterFactory接口就能实现大部分自定义的类型转换器,通常没必要实现GenericConverter来开发自定义的类型转换器。
下面只使用Converter接口即可开发自定义的类型转换器:
public class StringToAuthorConverter implements Converter<String, Author> { public Author convert(String s) { try { String args[] = s.split("-"); Author author = new Author(Integer.parseInt(args[0]), args[1], Integer.parseInt(args[2])); return author; } catch (Exception e) { e.printStackTrace(); return null; } } }
上面转换器实现了最简单的Converter<String,Author>接口,该转换器负责将String对象转换成Author对象;程序实现了该接口中的convert(String text)方法,该方法可以将传入的String对象转换成Author对象。该方法的实现逻辑很简单,它只是以“-”为分隔符,将用户输入的字符串分成三个字符串,其中第一个字符串作为Author的id属性,第二个子串作为Author的name属性,第三个子串作为Author的age属性。
接下来需要使用ConversionService加载、配置该转换器。ConversionService是Spring类型转换体系的核心接口,Spring也为它提供了一些实现类,但实际上并不直接配置ConversionService实现类,而是通过ConversionServiceFactoryBean来配置ConversionService。因为ConversionServiceFactoryBean实现了FactoryBean<ConversionService>接口,程序获取ConversionServiceFactoryBean时,实际返回的是它的产品:ConversionService。
此外,Spring还提供了一个FormattingConversionServiceFactoryBean,它是工厂Bean,它返回的产品是FormattingConversionService对象(ConversionService的实现类)。该转换器可通过如下两个注解执行类型转换:
- @DateTimeFormat:用于对Date类型执行转换。
- @NumberFormat:用于对Number及其子类执行转换。
通常推荐使用FormattingConversionServiceFactoryBean配置ConversionService。
<mvc:annotation-driven conversion-service="conversionService"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.ysy.springmvc.converter.StringToAuthorConverter"/> </set> </property> </bean>
上面最后代码中使用了FormattingConversionServiceFactoryBean配置了ConversionService,并通过它的converters属性添加了自定义的类型转换器——如果程序还需要更多的类型转换器,只要在此处列出即可。
在配置了ConversionService后,还需要为<mvc:annotation-driven.../>元素指定conversion-service属性,该属性引用项目容器中实际配置的ConversionService Bean。如果不指定该属性,那么<mvc:annotation-driven.../>将会继续使用它原来的ConversionService组件。
接下来需要处理Book的Date类型的publicDate属性,此时完全不需要开发额外的转换器,只要使用@DateTimeFormat即可——这就是ConversionService的优势。
@DateTimeFormat(pattern = "yyyy-MM-dd") private Date published;
上面代码使用@DateTimeFormat注解修饰了publishDate,并指定了pattern为“yyyy-MM-dd”,这样FormattingConversionService即可根据注解指定的日期模板对用户输入的日期字符进行转换,非常方便,无须开发额外的转换器。
对同一种类型的对象来说,如果既通过ConversionService加载了自定义的类型转换器,又通过WebBindingInitializer装配了全局的自定义PropertyEditor,同时还使用了InitBinder修饰的方法装配了自定义PropertyEditor,此时SpringMVC将按照一下的优先顺序查找对应的类型转换器:
- 查找@InitBinder修饰的方法装配的自定义PropertyEditor。
- 查找ConversionService加载的自定义的类型转换器。
- 查找通过WebBindingInitializer加载的全局的自定义PropertyEditor。
1.6,处理转换错误
从项目的实际运行角度来看,用户经常输入与程序要求不相符的格式,类型转换就会失败,就会出现400的报错界面,这样对用户来说就太不友好了。比较理想的是:当用户输入不符合格式、类型转换失败时,程序自动跳转回表单页面,并在表单页面显示错误提示信息。为了实现上面的处理流程,需要对程序增加如下两步:
- 修改控制器类的处理方法,在处理方法的表单对象参数之后增加一个BindingResult类型的参数,如果类型转换失败,转换失败的错误信息会被自动封装在BindingResult参数中。
- 在页面上使用<form:errors.../>标签输出类型转换失败的错误信息。
@Controller public class BookController { @GetMapping("/{url}") public String url(@PathVariable String url) { return url; } // @PostMapping指定该方法处理/addBook请求 // BindingResult参数必须紧跟在Book参数之后 @PostMapping("/addBook") public String add(Book book, BindingResult result, Model model) { if (result.getErrorCount() > 0) { for (FieldError error : result.getFieldErrors()) { System.out.println("-----------" + error); } return "bookForm"; } System.out.println("添加的图书:" + book); model.addAttribute("tip", book.getName() + "图书添加成功!"); return "success"; } }
上面程序的add()方法在Book book参数后增加了一个BindingResult类型的参数,其中Book book参数对应的表单对象,因此这个BindingResult参数必须紧跟在Book book参数之后。
当Book参数中某个信息转换失败后,错误信息会被封装为一个Filed对象,而BindingResult则封装了所有的FiledError。如果返回值大于0,则表明至少存在一个表单域的类型转换失败,于是该处理方法返回“bookForm”作为逻辑视图名,这意味着程序会再次跳转到表单页面。
<div class="container"> <img src="${pageContext.request.contextPath}/imgs/logo.gif" class="rounded mx-auto d-block"><h4>添加图书</h4> <form method="post" action="addBook"> <div class="form-group row"> <label for="name" class="col-sm-2 col-form-label">图书名:</label> <div class="col-sm-7"> <input type="text" id="name" name="name" value="${book.name}" class="form-control" placeholder="请输入图书名"> </div> <div class="col-sm-3 text-danger"> <form:errors path="book.name"/> </div> </div> <div class="form-group row"> <label for="publishDate" class="col-sm-2 col-form-label">出版日期</label> <div class="col-sm-7"> <input type="text" id="publishDate" name="publishDate" class="form-control" placeholder="请输入出版日期(yyyy-MM-dd)"> </div> <div class="col-sm-3 text-danger"> <form:errors path="book.published"/> </div> </div> <div class="form-group row"> <label for="author" class="col-sm-2 col-form-label">作者:</label> <div class="col-sm-7"> <input type="text" id="author" name="author" class="form-control" placeholder="请输入作者信息(ID-名字-年龄)"/> </div> <div class="col-sm-3 text-danger"> <form:errors path="book.author"/> </div> </div> <div class="form-group row"> <div class="col-sm-6 text-right"> <button type="submit" class="btn btn-primary">添加</button> </div> <div class="col-sm-6"> <button type="reset" class="btn btn-danger">重设</button> </div> </div> </form> </div>
2,格式化
Spring转换器接口中只定义了一个方法,这意味着这种类型转换是单向的。如果转化器实现了Converter<String, Author>接口,那么它就只能将String对象转换成Author对象,而不能将Author对象转换成String对象。对SpringMVC来说,它要处理的类型转换其实包括两个方向:
- 将String类型的请求参数转换为目标类型。
- 当程序需要在页面上展示时,还需要将目标类型转换成String类型。
Spring提供了格式化器(Formatter)来完成这种双向转换。格式化器虽然能支持两种类型的相互转换,但它只能支持String类型与其他类型之间的转换。
转换器 格式化器 是否支持双向转换 否 是 是否支持多种类型的转换 是 否 对比发现,转换器其实是一种更通用的类型转换器工具,它可以完成任意两种单向转换;而格式化器则更适用于SpringMVC层,因为当程序从ServletRequest获取的请求擦书都是String类型时,需要将这些String类型的参数转换为目标类型;当程序需要在页面上输出、展示Java对象时,就需要将目标类型对象转换为String对象。
如果单纯地完成两种类型转换之间的单向转换,比如将String类型的请求参数转换为目标类型,则可考虑实现Converter接口来实现转换器;但是SpringMVC应用中,通常需要实现String类型与目标类型之间的双向转换,此时建议使用Formatter接口来实现格式化器。
2.1,使用格式化器
使用格式化器质性类型转换同样只需要两步:
- 开发自定义的格式化器。自定义的格式化器应实现Formatter接口,并实现该接口中的两个方法。
- 使用FormattingConversionService配置自定义的转换器。FormattingConversionService其实是ConversionService的实现类。
Formatter<T>接口继承了Printer<T>和Parser<T>两个父接口。
Printer<T>接口中定义了如下方法:
String print(T object, Locale locale) //该方法完成从T类型到String类型的转换
Parser<T>接口中定义了如下方法:
T parse(String text, Locale locale) //该方法完成从String类型到T类型的转换
在实现Formatter接口时,必须实现如下两个方法的作用:
- print():该方法负责完成从目标类型到String类型的转换。
- parse():该方法负责完成从String类型到目标类型的转换。
public class AuthorFormatter implements Formatter<Author> { public Author parse(String text, Locale locale) throws ParseException { try { // 将传入的字符串参数以 – 为分割符,分割成字符串数组 String[] args = text.split("-"); // 以传入参数创建Author对象(即将传入参数转换为Author对象) Author author = new Author(Integer.parseInt(args[0]), args[1], Integer.parseInt(args[2])); // 返回转换结果:Author对象 return author; } catch (Exception ex) { throw new ParseException(ex.getMessage(), 46); } } public String print(Author author, Locale locale) { return author.getId() + "-" + author.getName() + "-" + author.getAge(); } }
上面AuthorFormatter类实现了Formatter<Author>接口,并实现了该接口中的print()和parse()两个方法,这样AuthorFormatter就可以作为格式化器使用,完成Author和String之间的双向转换。
接下来需要在SpringMVC配置文件中配置该格式化器。Converter和Formatter其实都属于Spring的ConversionService体系,因此都需要通过ConversionService进行配置,只不过在配置格式化器时必须使用ConversionService的实现类:FormattingConversionService,而实际配置时则使用它的工厂类:FormattingConversionServiceFoctoryBean。
在配置FormattingConversionServiceFactoryBean时,可通过converters属性来配置多个转换器,也可以通过formatters属性来配置多个格式化器。
<?xml version='1.0' encoding='UTF-8' ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 定义扫描装载的包 --> <context:component-scan base-package="com.ysy.springmvc.Controller"/> <mvc:annotation-driven conversion-service="conversionService"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/content/" p:suffix=".jsp"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <set> <bean class="com.ysy.springmvc.formatter.AuthorFormatter"/> </set> </property> </bean> </beans>
格式化器与转换器不同,格式化器可以完成目标类型与String类型之间的双向转换,因此它可以在页面输出时起作用。为了在页面输出时让格式化器发挥作用,在页面上应使用spring标签库的eval标签来计算、输出指定表达式的值。eval标签可以指定如下属性:
- expression:指定该标签要计算的表达式。
- var:如果指定该属性,则表明该标签计算的结果不在页面输出,而是以var指定的变量名保存起来。
- scope:该属性需要与var属性结合使用,scope属性指定要将var指定的变量存入application、session、request或page范围内。
- htmlEscape:指定是否对HTML代码执行转义。
- javaScriptEscape:指定是否对JS代码执行转义。
<body> <div class="container"> <div class="alert alert-primary">${tip}<br> 书名: ${book.name}<br> 出版日期: <spring:eval expression="book.publishDate"/><br> 作者: <spring:eval expression="book.author"/><br> </div> </div> </body>
2.2,使用FormatterRegistrar注册格式化器
FormattingConversionServiceFactoryBean除了可通过converters属性配置转换器、通过formatters属性配置格式化器之外,还提供了如下setter方法:
setFormatterRegistrars(Set<FormatterRegisterar> formatterRegistrars)
对于SpringBean而言,所有的setter方法都可配置设值,因此上面方法意味着FormattingConversionServiceFactoryBean可通过formatterRegistrars属性配置多个FormatterRegistrar。FormatterRegistrar是一个接口,该接口的实现类可通过实现registerFormatters(FormatterRegistryregistry)方法来注册多个格式化器。
简单来说,可以把FormatterRegistrar理解为一组格式化器,因此,当程序通过formatterRegistrars配置多个FormatterRegistrar会比较有用。此外,当使用XML声明注册不足以解决问题时,使用FormatterRegistrar也是比较好的替代方案。
public class AuthorFormatterRegistrar implements FormatterRegistrar { public void registerFormatters(FormatterRegistry formatterRegistry) { formatterRegistry.addFormatter(new AuthorFormatter()); } }
AuthorFormatterRegistrar类实现了FormatterRegister接口,并重写了该接口中的registerFormatters()方法,程序即可在该方法中注册多个格式化器——这样AuthorFormatterRegistrar就相当于组合了这些格式化器。
提供了FormatterRegistrar实现类之后,还需要把它们传给FormattingConversionServiceFactoryBean的formatterRegistrars属性。
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatterRegistrars"> <set> <bean class="com.ysy.springmvc.formatter.AuthorFormatterRegistrar"/> </set> </property> </bean>
3,数据校验
表现层另一个数据处理就是数据校验,数据校验可分为客户端校验和服务端校验两种。客户端校验和服务端校验都是必不可少的,二者分别完成不同的过滤:
- 客户端校验进行基本校验,如校验字段是否为空、数据格式是否正确等。客户端校验主要用来过滤用户的误操作。客户端校验的作用:拒绝操作错误输入被提交到服务端处理,降低服务端的负担。
- 服务端校验进行防止非法数据进入程序,以免导致程序异常、底层数据库异常。服务端校验是保证程序有效运行及数据完整的手段。
Spring支持的数据校验主要是执行服务端校验,客户端校验则可借助第三方JS框架来实现。Spring支持的服务端校验可分为两种:
- 使用Spring原生提供的Validation,这种校验方式需要开发者手动写校验代码,比较烦琐。
- 使用JSR 303校验机制,这种校验方式只需使用注解,即可声明式的方式进行校验,非常方便。
3.1,使用Validation执行校验
使用Validator执行校验的步骤很简单,只需两步:
- 实现Validator接口或SmartValidator接口编写校验器。
- 在控制器中使用@InitBinder方法注册校验器,并为处理方法的被校验参数添加@Valid或@Validated注解。
在编写校验器时必须实现Validator接口或SmartValidator接口,其中SmartValidator是Validator的子接口,增加了一些关于校验提示的支持,通常实现Validator接口就好。
在实现Validator接口时必须实现如下两个方法:
- boolean supports(Class<?> clazz):该方法的返回值决定该校验器能否对clazz类执行校验。
- void validate(Object target, Errors errors):该方法的代码对target执行实际校验,并使用Errors参数来收集错误信息。
编写校验器涉及一个Errors接口,该接口是前面介绍的BindingResult的父接口,因此,Errors同样也可用于封装FiledError对象。
Spring校验框架的常用接口和类总结如下:
- Errors:专门用于存储和暴露特定的对象的绑定、校验信息。
- BindingResult:Errors的子接口,主要增加了一些绑定信息分析和模型构建的功能。
- FiledError:封装一个表单域的类型转换失败或数据校验错误信息。每个FieldError对应一个表单域。
- ObjectError:FieldError的父类。
此外,Spring还为数据校验提供了一个ValidationUtils工具类,该工具类提供了一些rejectIfEmptyXxx()方法,用于对指定的表单域执行非空校验——当然,也可以不用这个工具类,这意味着必须自己去判断空字符串、空白内容等,这就比较烦琐了。
添加数据校验功能,先实现Validator接口或SmartValidator接口。
public class BookValidator implements Validator { public static final double MIN_PRICE = 50.0; public boolean supports(Class<?> aClass) { //判断Book是否为aClass类本身或其父类或aClass所实现的接口 return Book.class.isAssignableFrom(aClass); } public void validate(Object o, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "name", null, "图书名不能为空"); ValidationUtils.rejectIfEmpty(errors, "price", "priceNotEmpty", "图书价格不能为空"); Book book = (Book) o; // 要求name的字符长度不能少于6个字符 if (book.getName() != null && book.getName().length() < 6) { // 使用Errors的rejectValue方法验证 errors.rejectValue("name", null, "图书名长度至少包含6个字符"); } // 要求price不能小于50 if (book.getPrice() != null && book.getPrice() < MIN_PRICE) { errors.rejectValue("price", "price_too_low", new Double[]{MIN_PRICE}, "图书价格不能低于" + MIN_PRICE); } } }
上面校验器实现了Validator接口,并实现了该接口中的方法,其中实现supports()方法的目标就是判断目标类是否可由该校验器校验。该方法的实现代码比较简单,只要aClass参数是Book类或其子类,该校验器即可校验它。
程序中的validate()方法只校验了name、price两个表单域,因此代码相对比较简单,该方法的前两行代码用于调用ValidationUtils类对name、price两个表单域进行了非空校验。执行非空校验的方法有很多个重载版本,其中最完整的方法签名如下:
void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs, String defaultMessage)
- Errors errors:用于收集校验错误信息。
- String field:指定对哪个字段(表单域)执行非空校验。
- String errorCode:指定校验失败后错误信息的国际化消息的key。
- Object[] errorArgs:用于为国际化消息中的占位符填充参数值。
- String defaultMessage:如果没有国际化消息的key,或者key对应的国际化消息不存在,该参数指定的字符串会作为错误提示信息。
在理解了rejectIfEmpty()方法各种参数的作用后,可以看出validate()方法中没有为错误提示指定国际化消息的key,因此将使用defaultMessage参数作为校验失败的错误提示;第二行则指定了国际化消息的key,这样它就可以输出国际化的错误提示。
validate()方法后面两行代码调用了Errors对象的方法来添加校验失败信息。Errors同样提供了大量重载方法来添加校验失败信息,其中最完整的签名如下:
rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage)
这与前面的rejectIfEmpty()方法大致相同,只不过少了一个Errors参数,这是因为该方法本来就是由Errors对象调用的。
@Controller public class BookController { @GetMapping("/{url}") public String url(@PathVariable String url) { return url; } @InitBinder // 使用@InitBinder方法来绑定校验器 public void initBinder(WebDataBinder binder) { binder.replaceValidators(new BookValidator()); } // @PostMapping指定该方法处理/addBook请求 // BindingResult参数必须紧跟在Book参数之后 @PostMapping("/addBook") public String add(@Validated Book book, BindingResult result, Model model) { if (result.getErrorCount() > 0) { for (FieldError error : result.getFieldErrors()) { System.out.println("-----------" + error); } return "bookForm"; } System.out.println("添加的图书:" + book); model.addAttribute("tip", book.getName() + "图书添加成功!"); return "success"; } }
上面控制器类定义了一个@InitBinder修饰的方法,该方法的实现代码调用WebDataBinder参数的方法为控制器绑定了BookValidator校验器。控制器里最重要的就是book参数前面的@Validated注解,该注解告诉SpringMVC使用绑定的校验器对book参数进行数据校验。
@Validated注解和Java提供的@Valid注解功能类似,@Validated是Spring专门为@Valid提供的一个变体,因此使用起来比较方便。
与类型转换失败的处理类似,程序需要在处理方法表单对象参数只有紧跟一个BindingResult参数,该参数用于封装类型转换失败,Spring都会将失败信息封装成对应的FiledError,并添加到BindingResult参数中。
3.2,基于JSR 303执行校验
JSR 303规范叫做Bean Validation,它是Java EE6规范的重要组成部分。Bean Validation规范专门用于对Java Bean的属性值执行校验。它用起来非常简单,只要程序在Java Bean的成员变量或setter方法上添加类似于@NotNull、@Max的注解来指定校验规则,接下来标准的校验接口就能根据这些注解对Bean执行校验。
使用JSR 303的关键就是一套允许添加在成员变量或setter方法上的注解。下面是JSR 303提供的注解,位于javax.validation.constraints包下:
- @AssertFalse:要求被修饰的boolean类型的属性必须为false。
- @AssertTrue:要求被修饰的boolean类型的属性必须为true。
- @DecimalMax(value):要求被修饰的属性值不能大于该注解指定的值。
- @DecimalMin(value):要求被修饰的属性值不能小于该注解指定的值。
- @Digits(integer,fraction):要求被修饰的属性值必须具有指定的整数位和小数位数。其中integer指定整数位数,fraction指定小数位数。
- @Email:要求被修饰的属性值必须是有效的邮件地址。
- @Future(value):要求被修饰的Date或Calendar类型的属性值必须位于该注解指定的日志之后。
- @FutrueOrPresent:与@Futrue的区别是允许被修饰的属性值等于该注解指定的日期。
- @Max(value):要求被修饰的属性值不能大于该注解指定的值。
- @Min(value):要求被修饰的属性值不能小于该注解指定的值。
- @Negative:要求被修饰的属性值必须是负数。
- @NegativeOrZero:要求被修饰的属性必须是负数或零。
- @NotBlank:要求被修饰的String类型的属性不能为null,不能为空字符串,去掉前后空格之后也不能为空字符串。
- @NotEmpty:要求被修饰的集合类型的属性值不能为空集合。
- @NotNull:要求被修饰的属性必须不为NULL。
- @Null:要修被修饰的属性必须为null。
- @Past(value):要求被修饰的Date或Calendar类型的属性值必须位于该注解指定的日期之前。
- @PastOrPresent(value):与@Past的区别是,它允许被修饰的属性值等于该注解指定日期。
- @Pattern(regex):要求被修饰的属性值必须匹配该注解指定的正则表达式。
- @Positive:要求被修饰的属性必须为正数。
- @PositiveOrZero:要求被修饰的属性必须为正数或零。
- @Size(value):要求被修饰的集合类型的属性值包含的集合元素的个数必须在min~max范围之内。
此外,Hibernate Validator在org.hibernate.validator.constraints包下又补充了如下常用的支持:
- @CreditCardNumber:要求被修饰的属性必须是合法的信用卡卡号。
- @Currency:要求被修饰的属性值必须是合法的货币写法(必须有货币符号,而且写法要符合规范)。
- @ISBN:要求被修饰的属性值必须有全球有效的ISBN编号。
- @Length(min, max):要求被修饰的String类型的属性值的长度必须为min~max。
- @Range(min, max):要求被修饰的属性值必须为min~max。
- @URL:要求被修饰的属性值必须是一个有效的URL字符串。
在SpringMVC应用中使用JSR 303规范之前必须增加JSR 303规范实现。使用Hibernate Validator作为规范实现:下载Hibernate Validator的最新稳定版或在pom.xml添加如下代码。
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>6.1.6.Final</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.jboss.logging</groupId> <artifactId>jboss-logging</artifactId> <version>3.4.1.Final</version> </dependency>
接下来在Book类中增加JSR 303规范的注解:
public class Book { private Integer id; @NotBlank(message = "图书名不允许为空") @Length(min = 6, max = 30, message = "书名长度必须在6~30个字符之间") private String name; @Range(min = 50, max = 200) private Double price; @DateTimeFormat(pattern = "yyyy-MM-dd") @Past(message = "出版日期必须是一个过去的日期") private Date publishDate; @Email(message = "必须输入合法的邮件地址") private String email; @Pattern(regexp = "[1][3-8][0-9]{9}", message = "必须输入有效的手机号") private String phone; ...... }
从上面可以看出,使用JSR303规范的注解来执行数据校验非常方便,只要程序使用数据校验的注解来修饰这些变量即可。
接下来的控制器与上一个相似,同样使用@Validated修饰Book book参数,并在该参数后面添加BindingResult参数,用于收集数据校验失败的信息。
@Controller public class BookController { @GetMapping("/{url}") public String url(@PathVariable String url) { return url; } // @PostMapping指定该方法处理/addBook请求 // @Validated注解修饰的对象,表明该对象需要被校验 @PostMapping("/addBook") public String add(@Validated Book book, BindingResult result, Model model) { if (result.getErrorCount() > 0) { return "bookForm"; } System.out.println("添加的图书:" + book); model.addAttribute("tip", book.getName() + "图书添加成功!"); return "success"; } }
标签:格式化,String,SpringMVC,PropertyEditor,校验,接口,类型,属性 来源: https://blog.csdn.net/qq_42192693/article/details/117818732