其他分享
首页 > 其他分享> > SpringMVC:转换器、格式化器、数据校验

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