SpringMVC 自定义类型转换器及JSR303数据校验

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

一、类型转换器

在日常的企业开发需求中,我们输入文本框的内容全部都是字符串类型,但是在后端处理的时候我们可以用其他基本类型来接受数据,也可以使用实体类来接受参数,这个是怎么完成的呢?就是通过SpringMVC提供的类型转换器,SpringMVC内部提供了非常丰富的类型转换器的支持,但是有些情况下有可能难以满足我们的需求,因此需要我们自己实现

比如我们做一个数据采集平台,从不同的平台采集数据,拿日期来说,有的平台日期格式为:yyyy-MM-dd,有的平台为yyyy/MM/dd,当然也有可能是其他格式,为了实现日期的统一实现,我们就需要自定义一个类型转换器

1-1、创建一个转换器

我们目前这个转化器只支持 yyyy-MM-dd yyyy/MM/dd

package com.jony.converters;

import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MyStringToDateConverter implements Converter<String, Date> {
    @Override
    public Date convert(String source) {
        if(!StringUtils.isEmpty(source)){
            // 即支持yyyy-MM-dd  yyyy/MM/dd
            try {
                if(source.split("-").length==3){
                    DateFormat df=new SimpleDateFormat("yyyy-MM-dd");
                    return df.parse(source);
                }
                else if(source.split("/").length==3){
                    DateFormat df=new SimpleDateFormat("yyyy/MM/dd");
                    return df.parse(source);
                }
                else
                {
                    throw new RuntimeException("日期转换错误:"+source);
                }

            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
复制代码

1-2、创建一个User bean,并将生日设为日期格式

package com.jony.entity;

import org.junit.Test;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Date;

@Controller
@RequestMapping("/converter")
public class User {
    private String name;
    private Integer age;
    private Date birthday;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }
}
复制代码

1-3、在spring.mvc 中进行类转化器配置

<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>

<bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="conversionService">
    <property name="converters">
        <set>
            <bean class="com.jony.converters.MyStringToDateConverter"></bean>
        </set>
    </property>
</bean>
复制代码

1-4、测试

先使用yyyy/MM/dd

image.png 再次使用yyyy-MM-dd

image.png

1-5、总结

在实际项目中,我们需要根据实际情况,创建类转化器,这样就不需要在各个业务中再次进行处理。使用步骤如下:

1.定义类型转换器 需要明确 源类型 和目标性
2.在convert方法自定义类型转换的实现
3.在springmvc中配置自定类型转换器\

<bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="conversionService">
    <property name="converters">
        <set>
            <bean class="com.jony.converters.MyStringToDateConverter"></bean>
        </set>
    </property>
</bean>
复制代码

4.设置springmvc注解驱动

<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
复制代码

二、数据格式化

Spring 提供了两个可以用于格式化数字、日期和时间的注解@NumberFormat和@DateTimeFormat,这两个标签可以用于javabean的属性或方法参数上。@NumberFormat可以用来格式化任何的数字的基本类型(如int,long)或java.lang.Number的实例(如 BigDecimal, Integer)。@DateTimeFormat可以用来格式化java.util.Date、java.util.Calendar和 java.util.Long类型.

2-1、实际使用

package com.jony.entity;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

import java.util.Arrays;
import java.util.Date;

public class User {

    private Integer id;

    private String username;

    @DateTimeFormat(pattern="yyyy/MM/dd")
    private Date birthday;

    @NumberFormat(style = NumberFormat.Style.CURRENCY )   //货币
    private Double balance;   //余额  ¥5000

    private String[] hobbies;

    @NumberFormat(pattern = "#,###.##")
    private Double salary; //工资  10,000.00

    @NumberFormat(style = NumberFormat.Style.PERCENT)  /*不加%会 *100来显示   加上会按你提交精度来显示*/
    private Double taskCount;// 任务完成百分比 90%

    public String[] getHobbies() {
        return hobbies;
    }

    public void setHobbies(String[] hobbies) {
        this.hobbies = hobbies;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public Double getTaskCount() {
        return taskCount;
    }

    public void setTaskCount(Double taskCount) {
        this.taskCount = taskCount;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + ''' +
                ", birthday=" + birthday +
                ", balance=" + balance +
                ", hobbies=" + Arrays.toString(hobbies) +
                ", salary=" + salary +
                ", taskCount=" + taskCount +
                '}';
    }
}
复制代码

2-2、特点及使用方式

注解类型 DateTimeFormat,互斥属性

iso。类型为DateTimeFormat.ISO

  • DateTimeFormat.ISO.DATE: 格式yyyy-MM-dd。

  • DateTimeFormat.ISO.DATE_TIME: 格式yyyy-MM-dd HH:mm:ss .SSSZ。

  • DateTimeFormat.ISO.TIME: 格式HH:mm:ss.SSSZ。

  • DateTimeFormat.ISO.NONE: 表示不使用ISO格式的时间。

pattern。类型为String,使用自定义的时间格式化字符串。

style。类型为String,通过样式指定日期时间的格式,由两位字符组成,第1位表示日期的样式,第2位表示时间的格式:

  • S: 短日期/时间的样式;

  • M: 中日期/时间的样式;

  • L: 长日期/时间的样式;

  • F: 完整日期/时间的样式;

  • -: 忽略日期/时间的样式;

NumberFormat pattern。类型为String,使用自定义的数字格式化字符串,"##,###.##"。

style。类型为NumberFormat.Style,常用值:

  • Style.NUMBER正常数字类型

  • Style.PERCENT百分数类型

  • Style.CURRENCY 货币类型

三、数据校验

一般情况下我们会在前端页面实现数据的校验,但是大家需要注意的是前端校验会存在数据的不安全问题,因此一般情况下我们都会使用前端校验+后端校验的方式,这样的话既能够满足用户的体验度,同时也能保证数据的安全,下面来说一下在springmvc中如何进行后端数据校验。

3-1、JSR303

JSR303是 Java 为 Bean 数据合法性校验提供的标准,它已经包含在JavaEE 6.0 中 。JSR 303 (Java Specification Requests意思Java 规范提案)通过在Bean属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证。

Hibernate Validator(不属于Hibernate) 实现了JSR349验证注解规范的技术

JSR303:

image.png

Hibernate Validator 扩展注解:

image.png

3-2、JSR303的使用

spring中拥有自己的数据校验框架,同时支持JSR303标准的校验框架,可以在通过添加注解的方式进行数据校验。

实现数据校验的步骤:
1、加入org.hibernate包的依赖
2、这个地方需要注意的是,如果使用的是IDE,需要将新加入的jar包,放到WEB-INF/lib中。
3、在需要验证的javaBean的属性上假如对应的验证注解。
4、在需要验证的处理方法对应的javaBean参数上加上@Valid 在spring中本身没有提供JSR303的实现,需要导入依赖的包。
5、在需要验证的处理方法中假如BindingResult,代表自己处理错误,这样就会将错误信息显示到错误页面了。

3-2-1、引入jar包

<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>
复制代码

3-2-2、将新导入的包放到WEB-INF/lib中(IDE)

之前文章有描述,这边就不再演示了。

3-2-3、给javaBean属性添加注解

image.png

3-2-3、给方法参数的javaBean添加@Valid

image.png

3-2-4、访问测试

可以看到我们提交数据id为null的时候就会报错 image.png 然后修改ID为1

image.png

3-2-4、错误信息的处理

3-2-4-1、在控制层添加BindingResult

首先需要在方法中假如BindingResult类,这个类会接收到javaBean添加注解错误的信息。如果有错误信息可以通过getFieldErrors()获取错误信息集合,这时候也可以直接返回到页面输出,也可以将错误集合进行迭代放入map中,这样传到前台,就可以根据每个字段输出错误信息了

package com.jony.controller;

import com.jony.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
@RequestMapping("/valid")
public class ValidController {

    @RequestMapping("/user")
    public String add(@Valid User user, BindingResult bindingResult, Model model){
        //判断是否有错误
        if(bindingResult.hasErrors()){
            //创建一个map用于存放字段的错误
            Map<String,String> errMap=new HashMap<>();
            //获得所有字段的错误
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            //将错误信息迭代到map中
            for(FieldError fieldError:fieldErrors){
                errMap.put(fieldError.getField(),fieldError.getDefaultMessage());
            }
            //将错误信息放到model中返回到页面中
            model.addAttribute("errors",errMap);
            return "user/add";
        }
        System.out.println(user);
        return "show";
    }

}
复制代码

3-2-4-2、页面接收错误信息

我们在添加页面,设置一下每个标签的value值,这样即使有错误信息,从Controller再次跳转到页面的时候,也能把User带到视图层。 image.png

3-2-4-3、测试

image.png 可以看大由于ID不能为null,我们提交之后错误信息可以返回到页面,并且其他值的数据也并未丢失。

3-2-5、其他验证属性的使用

package com.jony.entity;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import java.util.Arrays;
import java.util.Date;

public class User {

    @NotNull(message = "id不能为空")
    @Min(value=1,message = "id必须大于0")
    private Integer id;

    @NotEmpty(message = "用户名不能为空")
    @Length(min = 4,max = 8,message = "用户名的长度必须在{min}和{max}位之间")
    @Pattern(regexp="^[0-9a-zA-Z]+$",message = "用户名只能输入0-9或者a-z")
    private String username;

    @Past(message = "您难道穿越了吗?")
    @DateTimeFormat(pattern="yyyy/MM/dd")
    private Date birthday;

    @NumberFormat(style = NumberFormat.Style.CURRENCY )   //货币
    private Double balance;   //余额  ¥5000

    private String[] hobbies;

    @NumberFormat(pattern = "#,###.##")
    @Range(min = 2000,max = 1000000,message = "工资必须在{min}和{max}之间")    //Size的范围是int 不能用于验证Double
    private Double salary; //工资  10,000.00
    
    @Range(min=0,max=100,message = "任务完成度必须在{min}和{max}之间")        //Range的范围是Long所以,Range可以控制的范围更大
    @NumberFormat(style = NumberFormat.Style.PERCENT)  /*不加%会 *100来显示   加上会按你提交精度来显示*/
    private Double taskCount;// 任务完成百分比 90%

    public String[] getHobbies() {
        return hobbies;
    }

    public void setHobbies(String[] hobbies) {
        this.hobbies = hobbies;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public Double getTaskCount() {
        return taskCount;
    }

    public void setTaskCount(Double taskCount) {
        this.taskCount = taskCount;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + ''' +
                ", birthday=" + birthday +
                ", balance=" + balance +
                ", hobbies=" + Arrays.toString(hobbies) +
                ", salary=" + salary +
                ", taskCount=" + taskCount +
                '}';
    }
}
复制代码

四、Springmvc form标签

什么使用html的标签接收错误信息比较繁琐,需要在Controller和视图端都做一些处理,鉴于此spring给我们提供了spring的form表单元素。

特点: 自动绑定,自动回显数据, 如果是新增的情况下也需要保证有该标签所需的对象

Form标签

  1. 支持全部http请求方法 比如method=”put” put\delete 提交方式

  2. 数据自动回显:需要使用modelAttribute指定数据的对象

  3. 使用path来双向绑定属性4. 动态数据绑定:Select 、checkboxes、 radiobottons、 都可以使用Items 制定数据源 可以是list (当List的泛型是javaBean的时候需要制定itemValue和itemLabel)、map(不需要制定itemValue和itemLabel)

4-1、原生html和spring form标签的实现区别

*  **基于原生html form表单实现方式**
复制代码
  • 1.在将错误信息循环通过map存入到request域中
  • 2.在jsp通过${errors.id}获取对应的错误信息
    • 基于spring form标签库的实现方式
  • 1.添加一个显示jsp的处理方法, 一定要传入一个空的User到model中
  • 2.在jsp中导入spring-form标签库
  • 3.在form标签上一定要加上ModelAtrribute
  • 4.加上对应的form标签 必须都要以<form:开头

4-2、spring form标签的实现过程

4-2-1、添加显示jsp的处理方法

这个必须传入User到mode中,之前提到 放到方法参数中可以直接传到视图中

/**
 * 跳转至add 页面
 * @param user
 * @return
 */
@RequestMapping("/u/add")
public String toAdd(User user){
    return "user/add";
}
复制代码

4-2-2、修改Controller

可以看到已经取消了之前迭代错误集合并放入map中的处理逻辑

/**
 * 使用spring form标签
 * @param user
 * @param bindingResult
 * @param model
 * @return
 */
@RequestMapping("/dataValidate")
public String addNew(@Valid User user, BindingResult bindingResult, Model model) {
    //判断是否有错误
    if (bindingResult.hasErrors()) {
        System.out.println("验证失败");
        return "user/add";
    }else{
        return "show";
    }
}
复制代码

4-2-3、jsp处理

4-2-3-1、导入spring-form标签库

<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
复制代码

4-2-3-2、form表单的创建

在form标签上一定要加上ModelAtrribute,然后标签都是以<form开头

<form:form action="${pageContext.request.contextPath}/form/user" method="post" modelAttribute="user">
    <p>
        id: <form:input path="id"></form:input><form:errors path="id"></form:errors>
    </p>
    <p>
        username: <form:input path="username"></form:input><form:errors path="username"></form:errors>
    </p>
    <p>
        birthday:<form:input path="birthday"></form:input><form:errors path="birthday"></form:errors>
    </p>
    <p>
        balance:<form:input path="balance"></form:input><form:errors path="balance"></form:errors>
    </p>
    <p>
        salary:<form:input path="salary"></form:input><form:errors path="salary"></form:errors>
    </p>
    <p>
        taskCount:<form:input path="taskCount"></form:input><form:errors path="taskCount"></form:errors>
    </p>
    <p>
        hobbies:
            <%--静态数据源--%>
            <form:checkbox path="hobbies" value="唱歌"></form:checkbox>
        <form:label path="hobbies">唱歌</form:label>
            <form:checkbox path="hobbies" value="跳舞"></form:checkbox>
        <form:label path="hobbies">跳舞</form:label>
    <hr>
    <%--动态数据源--%>
    <form:checkboxes path="hobbies" items="${list}"></form:checkboxes>

    <%--<form:select path="hobbies"></form:select>--%>
    </p>
    <p>
        <form:button>提交</form:button>
        <input type="submit" value="提交">
    </p>
</form:form>
复制代码

4-2-4、测试

一定要从Controller跳转到页面,并且带入User,因为我们表单中有个modelAttribute属性,指向user image.png

可以看到这样错误信息就可以成功显示了 image.png

4-2-5、数据动态绑定

动态数据绑定:Select 、checkboxes、 radiobottons、 都可以使用Items 制定数据源 可以是list(当List的泛型是javaBean的时候需要制定itemValue和itemLabel)、map(不需要制定itemValue和itemLabel)

4-2-5-1、修改跳转视图控制器、添加数据

/**
 * 跳转至add 页面
 * @param user
 * @return
 */
@RequestMapping("/u/add")
public String toAdd(User user,Model model){
    List<String> strings = Arrays.asList("唱歌", "跳舞");
    model.addAttribute("list",strings);
    return "user/add";
}
复制代码

4-2-5-2、测试

image.png 缺点:通过使用list想视图传值,value和lable就一样了,一般情况来说这样是不可行的,如下图

image.png

因此我们需要对数据进行修改,再次修改控制器如下:

   /**
     * 跳转至add 页面
     * @param user
     * @return
     */
    @RequestMapping("/u/add")
    public String toAdd(User user,Model model){
//        List<String> strings = Arrays.asList("唱歌", "跳舞");
//        model.addAttribute("list",strings);

        //使用map方式传值
        Map<String,String> map=new HashMap<>();
        map.put("1","唱歌");
        map.put("2","跳舞");
        model.addAttribute("list",map);
        return "user/add";
    }
复制代码

再次打开视图,这样value就是我们需要的值了。

image.png

猜你喜欢

转载自juejin.im/post/7083372084407042062