Spring 全家桶之 Spring Web MVC(五)- Data Binder

 2023-02-16
原文作者:RiemannHypothesis 原文地址:https://juejin.cn/post/7074536974131920904

“Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。”

一、Spring MVC 工程搭建

  1. 创建Maven项目,添加Spring MVC依赖
  2. 添加Web Application
  3. 配置web.xml
  4. 配置Spring MVC配置文件dispatcher-servlet.xml
  5. 新建controller,增加HelloController,增加hello方法,返回page/success.jsp页面
  6. 在WEB-INF下创建pages目录,该目录下新增加success.jsp
  7. 打开Artifact,新建lib包,将依赖全部导入lib包下
  8. 配置tomcat,启动Tomcat,输入localhost:8080/hello,成功返回success.jsp页面

二、Spring MVC 中数据绑定

Spring MVC会将页面请求的数据转换成自定义的类型,如将页面提交的POST表单数据"employeeName=stark&age=40&gender=1&email=stark@gmail.com"转换成Employee对象。

Spring MVC将自定义对象和页面请求绑定时主要涉及以下操作:

  • 数据类型转换,页面提交的都是字符串,要将字符串转换成自定义对象的不同类型的属性
  • 格式化问题,如日期格式的转换等
  • 数据校验,对页面提交的数据进行校验

Debug新增员工时,员工的email是如何赋值给Employee对象的

202301012036046451.png

    // 将页面提交的数据封装到Java Bean中
    bindRequestParameters(binder, request);

封装的过程中发生了数据转换、格式化和校验的操作

202301012036057992.png WebDataBinder:既数据绑定器,负责数据绑定工作,涉及了类型转换、格式化、数据校验等

  • conversionService组件:负责数据类型转换以及格式化
  • validators组件:负责数据校验
  • bindingResult组件:负责保存解析数据绑定期间数据校验产生的错误

Spring MVC数据绑定流程 Spring MVC通过反射机制对目标方法进行解析,将请求数据绑定到处理方法的入参中,数据绑定的核心是DataBinder。

202301012036086033.png

三、自定义类型转换器

ConversionService组件负责数据转换和格式化,ConversionService中有非常多的converter转换器,可以将页面提交的String类型数据转换成各种类型的数据,也可以通过实现Converter接口自定义类型转换器。

将spring-mvc-crud工程复制并重命名为spring-mvc-data;在list页面增加添加员工表单,向后台quickadd方法提交”empAdmin-admin@qq.com-1-101“这种类型的数据

    <form action="/quickadd">
        <%--将员工信息都填上,自动封装对象--%>
        <input name="empinfo" value="empAdmin-admin@qq.com-1-101">
        <input type="submit" value="快速添加">
    </form>

Controller方法中增加quickAdd方法

    @RequestMapping("/quickadd")
    public String quickAdd(@RequestParam("empinfo") Employee employee){
        // 输出的封装对象为null
        System.out.println("封装的对象:"+ employee);
        return "redirect:/emps";
    }

重启Tomcat,在list页面执行快速添加操作

202301012036093044.png

点击快速添加,页面报错Spring MVC无法将一段字符串“empAdmin-admin@qq.com-1-101”转换为Employee,这就需要自定义一个类型转换器,将String转换为Employee

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

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

3-1 实现自定义类型转换器

新建converter包,增加String转换为Employee的转换器类StringToEmployeeConverter

    public class StringToEmployeeConverter implements Converter<String, Employee> {
    
        @Autowired
        private DepartmentDao departmentDao;
    
        @Override
        public Employee convert(String source) {
            
            System.out.println("将提交的String类型转换为Employee类型");
            if (!source.contains("-")){
                return null;
            }
            String[] empInfo = source.split("-");
            Employee employee = new Employee();
            employee.setLastName(empInfo[0]);
            employee.setEmail(empInfo[1]);
    
            employee.setGender(Integer.parseInt(empInfo[2]));
    
            Department department = departmentDao.getDepartment(Integer.parseInt(empInfo[3]));
            employee.setDepartment(department);
            return employee;
        }
    }

通过实现Converter接口实现转换,接口的泛型分别为源数据类型和目标数据类型,convert方法返回要转换的类,通过将String类型拆分并赋值给新建的一个Employee对象实现String到Employee的转换。

注册自定义的转换器

    <mvc:annotation-driven conversion-service="conversionServiceFactory"></mvc:annotation-driven>
    
    <!--使用自定义的ConverterService-->
    <bean id="conversionServiceFactory" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <!--将自定义的转换器加入到converters中-->
        <property name="converters">
            <set>
                <bean class="com.citi.converter.StringToEmployeeConverter"></bean>
            </set>
        </property>
    </bean>

完成EmployeeController中的quickAdd方法

    @RequestMapping("/quickadd")
    public String quickAdd(@RequestParam("empinfo") Employee employee){
        // 输出的封装对象为null
        System.out.println("封装的对象:"+ employee);
        employeeDao.save(employee);
        return "redirect:/emps";
    }

重启Tomcat,执行添加操作

202301012036106175.png

自定义转换器步骤

  1. 实现Converter接口,实现convert方法
  2. 将自定义的Converter配置在ConversionService中
  3. 注册添加了自定义Converter的ConversionService

3-2 mvn:annotation-driven 标签

mvn:annotation-driver 标签支持以下这些功能:

  • 自动注册RequestMappingHandlerMapping、RequestMappingHandlerAdapter及ExceptionHandlerExceptionResolver三个组件
  • 支持ConversionService实例对表单参数进行类型转换
  • 支持@NumberFormat、@DataTimeFormat注解完成数据类型格式化
  • 支持@Valid注解对Bean进行数据校验
  • 支持@RequstBody和@ResponseBody注解

四、数据格式化及校验

4-1 数据格式化

  • Spring 在格式化模块中定义了一个实现ConversionService接口的FormattingConversionService实现类,该实现类扩展了GenericConversionService,因此它也具有类型转换的功能又具有格式化的功能。
  • FormattingConversionService拥有一个FormattingConversionServiceFactory工厂类,后者用于在Spring上下文中构造前者。

以时间格式化为例,在add.jsp页面添加员工表单增加birth属性

202301012036115766.png

list.jsp页面添加birth属性列,在Employee实体类增加birth属性

202301012036143777.png

运行添加员工,输入的时间格式为yyyy-MM-DD

202301012036171858.png

只有当时间格式为yyyy/MM/DD时才才能成功添加

202301012036181659.png

如何能正确处理yyyy-MM-DD?使用@DataTimeFormate注解可以指定日期的格式

在Employee实体类的birth属性上添加@DateTimeFormat注解,指定时间的格式

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;

添加还是失败,只是因为我们使用的conversionService不具备格式化功能,将 Spring MVC 配置文件中注册的

    org.springframework.context.support.ConversionServiceFactoryBean

换成

    org.springframework.format.support.FormattingConversionServiceFactoryBean
    <!--使用自定义的ConverterService-->
    <bean id="conversionServiceFactory" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!--中间内容不变-->
    </bean>

再次测试可以添加成功

2023010120362053910.png

FormattingConversionServiceFactoryBean内部注册了以下组件

  • NumberFormatAnnotationFormatterFactory:支持对数字类型的属性使用@NumberFormat注解
  • JodaDateTimeFormatAnnotationFormatterFactory:支持对日期类型的属性使用@DateTimeFormat注解

@NumerbFormat注解可以对数值类型的属性进行设置,该注解本身拥有两个属性 style:类型为NumberFormat.Style,用于指定样式类型,有三种:Style.NUMBER正常数字类型、Style.CURRENCY既货币类型、Style.PERCENT百分比类型 pattern:类型为String,可以自定义样式,如“#,###,###”等

4-2 数据校验

JSR 既 Java Specification Requests,Java规范请求,是Java为Bean数据合法性校验提供的标准框架,JSR 通过在Bean的属性上增加@NotNull、@Null、@Max等注解来指定属性的校验规则,并且自定义校验失败的提示信息。JSR是一套校验规范,Hibernate Validator实现了JSR,并且扩展了@Email、@Length、@NotEmpty、@Range注解。

关于校验框架的介绍及使用也可以参考你有没有使用过这些编程骚操作(二)- 验证框架  Part A、B、C 三部分

在Spring MVC中使用校验框架首先要引入validation依赖和hibernate实现以及el表达式的规范和实现

    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.16.Final</version>
    </dependency>
    <dependency>
        <groupId>javax.el</groupId>
        <artifactId>javax.el-api</artifactId>
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.web</groupId>
        <artifactId>javax.el</artifactId>
        <version>2.2.6</version>
    </dependency>

添加依赖后,要将Jar包全部PUT 进 lib目录下

2023010120362155911.png

在Employee实体类的属性上增加校验的注解

    @NotEmpty
    @Length(min = 5,max = 8)
    private String lastName;
    
    @Email
    private String email;

在方法入参中加入@Valid注解,对出传来的参数进行校验,并使用BindingResult来保存校验的结果

    @RequestMapping(value = "/emp", method = RequestMethod.POST)
    public String addEmp(@Valid Employee employee, BindingResult result){
    
        System.out.println("要添加的员工信息:" + employee);
        boolean hasErrors = result.hasErrors();
        if (hasErrors){
            System.out.println("属性校验错误");
            return "add";
        }
        employeeDao.save(employee);
        // 返回列表页面
        return "redirect:/emps";
    }

在jsp页面中解析校验结果

    LastName:<form:input path="lastName"/><form:errors path="lastName" /><br/>
    Email:<form:input path="email"/><form:errors path="email" /><br/>
    <!--其他内容不变-->
    birth:<form:input path="birth" /><form:errors path="birth" /><br>

重启Tomcat,执行添加操作

2023010120362419612.png

如何通过其他方式取出错误信息

修改addEmp方法,通过BindingResult的getFieldErrors方法取出具体的错误信息,并输出错误代码

    @RequestMapping(value = "/emp", method = RequestMethod.POST)
    public String addEmp(@Valid Employee employee, BindingResult result, Model model){
        Map<String, Object> errorMap = new HashMap<>();
    
        System.out.println("要添加的员工信息:" + employee);
        boolean hasErrors = result.hasErrors();
        if (hasErrors){
            System.out.println("属性校验错误");
            // 获取详细报错信息
            List<FieldError> fieldErrors = result.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                System.out.println("出错字段为:" + fieldError.getField() + ",报错信息为:" + fieldError.getDefaultMessage());
                // 获取错误代码,用来作为国际化配置的Key
                String[] codes = fieldError.getCodes();
                for (String code : codes) {
                    System.out.println(code);
                }
                errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
    
            }
            model.addAttribute("errorMap", errorMap);
            return "add";
        }
        System.out.println(result);
        employeeDao.save(employee);
        // 返回列表页面
        return "redirect:/emps";
    }

重启Tomcat,在执行添加操作

2023010120362582413.png

每个属性在数据绑定和数据校验发生错误时,都会生成一个FieldError对象 当一个属性校验失败后,校验框架会为该属性生成4个消息代码,这些代码以校验注解类名为前缀,结合modelAttribute、属性名以及属性类型名生成多个对应的消息代码

输出的错误信息都是英文,可以进行国际化配置根据浏览器的语言显示报错信息,当使用Spring MVC 时,Spring MVC会查看Web上下文是否配置了国际化消息,如果有则显示国际化消息,如果没有显示默认的信息

在resources目录下新增中文和英文的国际化配置文件error_zh_CN.properties和error_en_US.properties

    Email.email=email wrong, retry
    NotEmpty=can't be empty
    Length.java.lang.String=length incorrect
    Past=must be a past time
    Email.email=\u90AE\u7BB1\u4E0D\u5BF9
    NotEmpty=\u4E0D\u80FD\u4E3A\u7A7A
    Length.java.lang.String= \u957F\u5EA6\u4E0D\u5BF9
    Past=\u65F6\u95F4\u5FC5\u987B\u662F\u8FC7\u53BB\u7684
    typeMismatch.birth=\u751F\u65E5\u7684\u683C\u5F0F\u4E0D\u6B63\u786E

在Spring MVC 配置文件中增加国际化配置

    <!--国际化配置-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="error"></property>
    </bean>

重启Tomcat,执行添加操作

2023010120362746914.png

如何将错误的字段值回显到报错信息中?

以中文的国际化配置文件为例

    Email.email=\u90AE\u7BB1\u4E0D\u5BF9 - {0} 
    NotEmpty=\u4E0D\u80FD\u4E3A\u7A7A - {0} 
    Length.java.lang.String= \u957F\u5EA6\u4E0D\u5BF9 - {0} {1} {2}
    Past=\u65F6\u95F4\u5FC5\u987B\u662F\u8FC7\u53BB\u7684 - {0}
    typeMismatch.birth=\u751F\u65E5\u7684\u683C\u5F0F\u4E0D\u6B63\u786E - {0}
  • {0}:永远表示当前校验的属性名
  • {1}和{2}表示注解中设置的属性的value,取得顺序是按照字母排列来的

重新启动Tomcat,执行添加操作

2023010120362902015.png