⭐表示重要。
第一章:概述
- SpringMVC 将『把请求参数注入到POJO对象』这个操作称为『
数据绑定
』,英文单词是 binding。 - 数据类型的转换和格式化就发生在数据绑定的过程中,数据类型的转换和格式化是密不可分的两个过程,很多带格式化的数据必须明确的指定格式之后才能进行数据类型转换,最典型的就是『日期类型』。
第二章:自动类型转换
- HTTP 协议是一个无类型的协议,我们在服务器端接收到请求参数等形式的数据的时候,本质上都是字符串类型,参考 javax.servlet.ServletRequest 接口中获取全部请求参数的方法:
public Map<String, String[]> getParameterMap();
- 但是,在我们的实体类对象中需要的类型非常丰富,因此,SpringMVC 对基本数据类型提供了自动的类型转换。例如:请求参数是
"100"
字符串,我们实体类中需要的是 Integer 类型,那么 SpringMVC 会自动将字符串转换为 Integer 类型注入实体类。
第三章:日期和数值类型(⭐)
3.1 概述
- SpringMVC 提供了
@DateTimeFormat
注解和@NumberFormat
注解实现了在入参格式化场景下将字符串
转换为日期类型
或数值类型
。 - SpringMVC 提供了
@JsonFormat
注解实现 了在出参格式化场景下将日期类型
转换为字符串
,需要 jackson 的支持。
3.2 日期和数值类型的使用
- ① 实体类:
package com.github.fairy.era.mvc.entity;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 10:12
*/
public class Product implements Serializable {
// 通过注解设定数据格式
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date productDate;
// 通过注解设定数据格式
@NumberFormat(pattern = "###,###,###.###")
private Double productPrice;
public Date getProductDate() {
return productDate;
}
public void setProductDate(Date productDate) {
this.productDate = productDate;
}
public Double getProductPrice() {
return productPrice;
}
public void setProductPrice(Double productPrice) {
this.productPrice = productPrice;
}
@Override
public String toString() {
return "Product{" +
"productDate=" + productDate +
", productPrice=" + productPrice +
'}';
}
}
- ② 表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<form method="post" th:action="@{/save/product}">
生产日期:<input name="productDate" type="text" value="1992-10-15 17:15:06"/><br/>
产品价格:<input name="productPrice" type="text" value="111,222,333.444"/><br/>
<button type="submit">保存</button>
</form>
</body>
</html>
- ③ handler 方法:
package com.github.fairy.era.mvc.handler;
import com.github.fairy.era.mvc.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 10:46
*/
@Controller
public class ProductHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/save/product")
public String saveProduct(Product product) {
logger.info("ProductHandler.saveProduct的请求参数{}", product);
return "target";
}
}
3.3 转换失败后的处理方式
3.3.1 默认结果
- 实际开发中,当然不能将错误抛给用户。
3.3.2 BindingResult 接口
- BindingResult 接口和其父接口 Errors 中定义了很多和数据绑定相关的方法,如果在数据绑定过程中发生了错误,那么就可以通过这个接口类型的对象获取到相关的错误信息。
3.3.3 重构 handler 方法
package com.github.fairy.era.mvc.handler;
import com.github.fairy.era.mvc.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 10:46
*/
@Controller
public class ProductHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/save/product")
@ResponseBody
public String saveProduct(Product product,
// 在实体类参数和 BindingResult 之间不能有任何其他参数
// 封装数据绑定结果的对象
BindingResult result) {
// 如果数据绑定过程中发生了错误
if (result.hasErrors()) {
// 如果发生了错误,就跳转到专门显示错误信息的页面
// 相关的错误信息会自动放到请求域中,当然,也可以手动获取
String errorMessage = Optional.ofNullable(result.getFieldError()).map(FieldError::getDefaultMessage).stream().collect(Collectors.joining(","));
logger.error("ProductHandler.saveProduct.errorMessage = {}", errorMessage);
return "error";
}
logger.info("ProductHandler.saveProduct的请求参数是{}", product);
return "target";
}
}
3.3.4 在页面上显示错误消息
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
</head>
<body>
<!-- th:errors 属性:用来显示请求处理过程中发生的错误 -->
<!-- th:errors 属性值:访问错误信息的表达式 -->
<!-- 访问错误信息的表达式:访问请求域,需要使用 ${} 格式 -->
<!-- 访问请求域使用的属性名:执行数据绑定的实体类的简单类名首字母小写 -->
<!-- 具体错误信息:找到实体类之后进一步访问出问题的属性名 -->
<p th:errors="${product.productDate}">这里显示具体错误信息</p>
</body>
</html>
第四章:自定义类型转换器
4.1 概述
- 在实际开发过程中,难免会出现某些情况需要自定义类型转换器,因为我们自己自定义的类型在 SpringMVC 中没有对应的内置类型转换器,此时需要我们提供自定义类型转换器来执行转换。
4.2 需求
- 希望将表单中的
aaa,bbb,ccc
封装到实体类对象 Student 中的 address属性中。
4.3 创建实体类
实体类 Student 中有一个属性 address,其类型为 Address 。
示例:
- Address.java
package com.github.fairy.era.mvc.entity;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 13:36
*/
public class Address {
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 街道
*/
private String street;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
- Student.java
package com.github.fairy.era.mvc.entity;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 13:36
*/
public class Student {
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "Student{" +
"address=" + address +
'}';
}
}
4.4 表单
希望通过一个文本输入约定指定格式的字符串,然后转换为我们需要的类型,此时就必须通过自定义类型转换器来实现,因为 SpringMVC 内部没有这样的类型转换器。
示例:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h3>自定义类型转换器</h3>
<form method="post" th:action="@{/save/student}">
地址:<input name="address" type="text" value="aaa,bbb,ccc"/><br/>
<button>提交</button>
</form>
</body>
</html>
4.5 handler 方法
- 示例:
package com.github.fairy.era.mvc.handler;
import com.github.fairy.era.mvc.entity.Student;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 13:39
*/
@Controller
public class StudentHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@PostMapping("/save/student")
public String saveStudent(Student student) {
logger.info("StudentHandler类的saveStudent方法的请求参数是:{}", student);
return "target";
}
}
- 在目前代码的基础上,我们没有提供自定义的类型转换器,所以在处理请求的时候会看到如下的错误日志:
[13:42:31.607] [DEBUG] [http-nio-8080-exec-6] [org.springframework.web.servlet.DispatcherServlet] [POST “/ajax/save/student”, parameters={masked}]
[13:42:31.640] [WARN ] [http-nio-8080-exec-6] [org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver] [Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errorsField error in object ‘student’ on field ‘address’: rejected value [aaa,bbb,ccc]; codes [typeMismatch.student.address,typeMismatch.address,typeMismatch.com.github.fairy.era.mvc.entity.Address,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.address,address]; arguments []; default message [address]]; default message [Failed to convert property value of type ‘java.lang.String’ to required type ‘com.github.fairy.era.mvc.entity.Address’ for property ‘address’; nested exception is java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘com.github.fairy.era.mvc.entity.Address’ for property ‘address’: no matching editors or conversion strategy found]]]
[13:42:31.640] [DEBUG] [http-nio-8080-exec-6] [org.springframework.web.servlet.DispatcherServlet] [Completed 400 BAD_REQUEST]
- 当然,页面会抛出 400 错误。
4.6 创建自定义类型转换器
- 实现接口:
org.springframework.core.convert.converter.Converter<S,T>
。 其中,
S
是源类型(本例中是 String 类型),T
是目标类型(本例中是 Address 类型)。示例:
package com.github.fairy.era.mvc.converter;
import com.github.fairy.era.mvc.entity.Address;
import org.springframework.core.convert.converter.Converter;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-16 13:53
*/
public class String2AddressConverter implements Converter<String, Address> {
@Override
public Address convert(String source) {
// 1.按照约定的规则拆分源字符串
String[] split = source.split(",");
// 2.根据拆分结果创建 Address 对象
Address address = new Address();
address.setProvince(split[0]);
address.setCity(split[1]);
address.setStreet(split[2]);
// 3.返回转换得到的对象
return address;
}
}
4.7 在 SpringMVC 中注册
- springmvc.xml
<mvc:annotation-driven conversion-service="formattingConversionService"/>
<!-- 在 FormattingConversionServiceFactoryBean 中注册自定义类型转换器 -->
<bean id="formattingConversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 在 converters 属性中指定自定义类型转换器 -->
<property name="converters">
<set>
<bean class="com.github.fairy.era.mvc.converter.String2AddressConverter"></bean>
</set>
</property>
</bean>
- 完整的 springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<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="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 自动扫描包 -->
<context:component-scan base-package="com.github.fairy.era.mvc.handler" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 配置视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!-- 物理视图:视图前缀+逻辑视图+视图后缀 -->
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML5"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<mvc:annotation-driven conversion-service="formattingConversionService"/>
<!-- 在 FormattingConversionServiceFactoryBean 中注册自定义类型转换器 -->
<bean id="formattingConversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 在 converters 属性中指定自定义类型转换器 -->
<property name="converters">
<set>
<bean class="com.github.fairy.era.mvc.converter.String2AddressConverter"></bean>
</set>
</property>
</bean>
<mvc:default-servlet-handler/>
<mvc:view-controller path="/" view-name="portal"/>
</beans>