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";
    }
}

 

原文地址:https://blog.csdn.net/qq_42192693/article/details/117818732

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


开发过程中是不可避免地会出现各种异常情况的,例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题,甚至直接导致程序崩溃。因此,在开发过程中,合理处理异常、避免异常产生、以及对异常进行有效的调试是非常重要的。 对于异常的处理,一般分为两种方式: 编程式异常处理:是指在代
说明:使用注解方式实现AOP切面。 什么是AOP? 面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 通俗描述:不通过修改源代码方式,在主干功能里面添加新功能。 AOP底层使用动态代理。 AOP术语 连接点
Spring MVC中的拦截器是一种可以在请求处理过程中对请求进行拦截和处理的机制。 拦截器可以用于执行一些公共的操作,例如日志记录、权限验证、数据转换等。在Spring MVC中,可以通过实现HandlerInterceptor接口来创建自定义的拦截器,并通过配置来指定拦截器的应用范围和顺序。 S
在 JavaWeb 中,共享域指的是在 Servlet 中存储数据,以便在同一 Web 应用程序的多个组件中进行共享和访问。常见的共享域有四种:ServletContext、HttpSession、HttpServletRequest、PageContext。 ServletContext 共享域:
文件上传 说明: 使用maven构建web工程。 使用Thymeleaf技术进行服务器页面渲染。 使用ResponseEntity实现下载文件的功能。 @Controller public class FileDownloadAndUpload { @GetMapping(&quot;/file/d
创建初始化类,替换web.xml 在Servlet3.0环境中,Web容器(Tomcat)会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringS
在 Web 应用的三层架构中,确保在表述层(Presentation Layer)对数据进行检查和校验是非常重要的。正确的数据校验可以确保业务逻辑层(Business Logic Layer)基于有效和合法的数据进行处理,同时将错误的数据隔离在业务逻辑层之外。这有助于提高系统的健壮性、安全性和可维护
什么是事务? 事务(Transaction)是数据库操作最基本单元,逻辑上一组操作,要么都成功,要么都失败,如果操作之间有一个失败所有操作都失败 。 事务四个特性(ACID) 原子性 一组操作要么都成功,要么都失败。 一致性 一组数据从事务1合法状态转为事务2的另一种合法状态,就是一致。 隔离性 事
什么是JdbcTemplate? Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作。 准备工作 引入jdbcTemplate的相关依赖: 案例实操 创建jdbc.properties文件,配置数据库信息 jdbc.driver=com.mysql.cj.
SpringMVC1.MVC架构MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范是将业务逻辑、数据、显示分离的方法来写代码MVC主要作用是:降低了视图和业务逻辑之间的双向耦合MVC是一个架构模型,不是一种设计模式。1.model(模型)数据模型,提供要展示的数据,因此包
SpringMVC学习笔记1.SpringMVC应用1.1SpringMVC简介​SpringMVC全名叫SpringWebMVC,是⼀种基于Java的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于SpringFrameWork的后续产品。​MVC全名是ModelViewController,是模型(model)-视图(view)-控制器(co
11.1数据回显基本用法数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用Ajax来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。11.1.1简单数据类型简单数据类型,实际上框架在这里没有
一、SpringMVC简介1、SpringMVC中重要组件DispatcherServlet:前端控制器,接收所有请求(如果配置/不包含jsp)HandlerMapping:解析请求格式的.判断希望要执行哪个具体的方法.HandlerAdapter:负责调用具体的方法.ViewResovler:视图解析器.解析结果,准备跳转到具体的物
1.它们主要负责的模块Spring主要应用于业务逻辑层。SpringMVC主要应用于表现层。MyBatis主要应用于持久层。2.它们的核心Spring有三大核心,分别是IOC(控制反转),DI(依赖注入)和AOP(面向切面编程)。SpringMVC的核心是DispatcherServlet(前端控制器)。MyBatis的核心是ORM(对
3.注解开发Springmvc1.使用注解开发要注意开启注解支持,2.注解简化了,处理映射器和处理适配器,只用去管视图解析器即可案例代码:1.web.xml,基本不变可以直接拿去用<!--调用DispatcherServlet--><servlet><servlet-name>springmvc</servlet-name>
拦截器概述SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。**过滤器与拦截器的区别:**拦截器是AOP思想的具体应用。过滤器servlet规范中的一部分,任何javaweb工程都可以使用
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:xsi="
学习内容:1、SSH&SSM2、Spring3、Struts2&SpringMVC4、Hibernate&MyBatis学习产出:1.SSH和SSM都是有Spring框架的,他们两个差不多。2.Spring分为四个模块,持久层,表示层,检测层,还有核心层,核心层分为2个关键核心功能。分别为,控制反转(IOC),依赖注入(DI),和面向切面编程
一、SpringMVC项目无法引入js,css的问题具体原因是css和js等被SpringMVC拦截了:解决方案:在spring-mvc.xml中配置<mvc:default-servlet-handler/><?xmlversion="1.0"encoding="UTF-8"?><beansxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
开发环境:Eclipse/MyEclipse、Tomcat8、Jdk1.8数据库:MySQL前端:JavaScript、jQuery、bootstrap4、particles.js后端:maven、SpringMVC、MyBatis、ajax、mysql读写分离、mybatis分页适用于:课程设计,毕业设计,学习等等系统介绍