0%

数据绑定

Spring MVC 数据绑定详解:从流程到自定义类型转换

Spring MVC 的数据绑定机制是连接 HTTP 请求与业务方法参数的核心桥梁,它自动将请求中的参数(如 URL 路径、表单、请求头)转换为业务方法所需的 Java 类型(如 IntegerUser 自定义对象),并支持数据校验。从 “数据绑定全流程→核心组件 ConversionService→自定义类型转换器” 三个维度,彻底讲透数据绑定的实现原理与实践。

数据绑定核心流程:从请求参数到方法入参

Spring MVC 数据绑定的本质是 “将 Servlet 请求信息转换为目标方法入参对象”,整个流程由 WebDataBinder 主导,配合 ConversionService(类型转换)和 Validator(数据校验)完成,共 4 个关键步骤:

步骤 1:创建 DataBinder 实例

  • 触发点:DispatcherServlet 调用 HandlerAdapter(如 RequestMappingHandlerAdapter)后,HandlerAdapter 会获取 WebDataBinderFactory 实例;
  • 核心操作WebDataBinderFactory 根据当前请求 ServletRequest 和目标方法入参类型,创建 DataBinder 实例(默认实现 ServletRequestDataBinder);
  • DataBinder 作用:作为数据绑定的 “工作器”,负责后续的参数提取、类型转换、数据校验。

步骤 2:DataBinder 提取请求参数并转换类型

  • 参数提取DataBinderServletRequest 中提取请求参数(如 request.getParameter("username")、URL 路径参数、JSON 请求体);
  • 类型转换DataBinder 调用 ConversionService,将提取的字符串参数转换为目标入参类型(如将请求中的 "2024" 转换为 Integer,将 "1:zhangsan" 转换为 User 对象);
  • 参数填充:转换后的参数值被填充到目标入参对象中(如 User 对象的 idname 字段)。

步骤 3:Validator 进行数据合法性校验

  • 校验触发:若目标入参对象添加了校验注解(如 @NotNull@Size),DataBinder 会调用 Spring 上下文的 Validator 组件(默认 LocalValidatorFactoryBean)进行校验;
  • 校验结果:校验结果被封装到 BindingResult 对象中,包含 “校验通过 / 失败” 状态和错误信息(如 “用户名长度不能小于 3”)。

步骤 4:绑定结果传递给目标方法

  • 参数注入:Spring MVC 将 “转换后的入参对象” 和 “BindingResult 校验结果” 注入到目标业务方法的参数中;
  • 开发者处理:开发者可通过 BindingResult 判断校验结果,若有错误则返回错误页面或提示,若无错误则执行业务逻辑。
流程总结(简化)
请求参数(String)
DataBinder 提取参数
ConversionService 类型转换
目标入参对象(如 User)
Validator 数据校验
BindingResult 封装校验结果
入参对象 + BindingResult 注入方法

核心组件:ConversionService 类型转换体系

ConversionService 是 Spring MVC 类型转换的 “核心接口”,定义了类型转换的规范,支持内置转换器和自定义转换器,是数据绑定的关键依赖。

1. ConversionService 核心接口定义

ConversionService 接口包含 4 个核心方法,按功能可分为 “转换能力判断” 和 “实际转换操作” 两类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface ConversionService {
// 1. 判断是否能将源类型(Class)转换为目标类型(Class)
boolean canConvert(Class<?> sourceType, Class<?> targetType);

// 2. 判断是否能将源类型(带上下文)转换为目标类型(带上下文)
// TypeDescriptor:描述类型的上下文信息(如字段注解、泛型类型)
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

// 3. 将源对象转换为目标类型(Class)
<T> T convert(Object source, Class<T> targetType);

// 4. 将源对象转换为目标类型(带上下文),支持复杂类型(如泛型、注解驱动转换)
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
  • 关键区别:带 TypeDescriptor 的方法支持 “上下文感知” 的转换(如根据字段上的 @DateTimeFormat 注解转换日期格式),功能更强大,是 Spring MVC 内部实际使用的方法。

2. ConversionServiceFactoryBean:创建转换服务

Spring 通过 ConversionServiceFactoryBean 简化 ConversionService 的创建,它会:

  1. 创建默认的 ConversionService 实现(DefaultConversionService);
  2. 注册 Spring 内置的类型转换器(如 String→IntegerString→BooleanDate→String);
  3. 支持注册自定义转换器。
核心源码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {
private Set<?> converters; // 存储自定义转换器
private GenericConversionService conversionService;

@Override
public void afterPropertiesSet() {
// 1. 创建默认 ConversionService(DefaultConversionService),注册内置转换器
this.conversionService = createConversionService();
// 2. 注册用户配置的自定义转换器
ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
}

// 创建默认 ConversionService,内置大量基础转换器
protected GenericConversionService createConversionService() {
return new DefaultConversionService();
}

// 省略 getter/setter...
}
  • 内置转换器DefaultConversionService 初始化时会注册超过 100 个内置转换器,覆盖大部分基础类型转换(如 StringNumberBooleanEnum 的互转,CollectionArray 的互转)。

自定义类型转换器:3 种实现方式

当 Spring 内置转换器无法满足需求(如自定义对象 User 的转换、特殊日期格式转换)时,可通过以下 3 种方式实现自定义转换器,按复杂度从低到高排序。

方式 1:实现 Converter<S, T> 接口(单类型转换)

Converter<S, T> 是最简单的转换器接口,用于 “将一种源类型 S 转换为一种目标类型 T”,适合一对一的简单转换场景(如 String→UserString→LocalDate)。

接口定义
1
2
3
4
public interface Converter<S, T> {
// 将源类型 S 的对象转换为目标类型 T 的对象
T convert(S source);
}
实战示例:String→User 转换器

假设前端传递用户信息的格式为 id:name(如 "1:zhangsan"),需转换为 User 对象:

步骤 1:实现 Converter 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component // 注入 Spring 上下文
public class StringToUserConverter implements Converter<String, User> {

@Override
public User convert(String source) {
// 1. 校验源字符串合法性
if (source == null || source.trim().isEmpty()) {
return null;
}

// 2. 按约定格式拆分参数(示例格式:id:name)
String[] parts = source.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("用户格式错误,需满足 'id:name'");
}

// 3. 转换并封装为 User 对象
User user = new User();
// 处理 id 转换(支持 "null" 字符串表示空)
user.setId("null".equals(parts[0]) ? null : Integer.parseInt(parts[0]));
// 处理 name(支持 "null" 字符串表示空)
user.setName("null".equals(parts[1]) ? null : parts[1]);

return user;
}
}

// 自定义 User 类
public class User {
private Integer id;
private String name;
// getter/setter + toString()
}
步骤 2:配置 ConversionService 注册转换器

方式 A:XML 配置(传统方式)

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 1. 配置 ConversionService,注册自定义转换器 -->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<!-- 引用自定义转换器 Bean -->
<ref bean="stringToUserConverter"/>
</list>
</property>
</bean>

<!-- 2. 告知 Spring MVC 使用自定义的 ConversionService -->
<mvc:annotation-driven conversion-service="conversionService"/>

方式 B:Java 配置(推荐,Spring 3.2+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConversionServiceFactoryBean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc // 启用 Spring MVC 注解支持
public class WebConfig {

// 1. 配置 ConversionService,注册自定义转换器
@Bean
public ConversionService conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
// 添加自定义转换器
factoryBean.setConverters(Set.of(new StringToUserConverter()));
factoryBean.afterPropertiesSet(); // 初始化 ConversionService
return factoryBean.getObject();
}

// 2. 告知 Spring MVC 使用自定义 ConversionService(若不配置,默认使用 DefaultConversionService)
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 如需自定义 JSON 转换器(如 FastJson),可在此配置
}
}
步骤 3:测试转换器

在 Controller 方法中直接使用 User 作为入参,Spring MVC 会自动调用自定义转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

// 请求示例:http://localhost:8080/testUser?user=1:zhangsan
@GetMapping("/testUser")
public String testUser(@RequestParam("user") User user) {
System.out.println("转换后的 User:" + user); // 输出:User(id=1, name=zhangsan)
return "转换成功:" + user;
}
}

方式 2:实现 ConverterFactory<S, R> 接口(同系列类型转换)

ConverterFactory 用于 “将一种源类型 S 转换为同一基类 R 的多个子类”,适合同系列类型的批量转换(如 String 转换为 Number 的子类 IntegerLongDouble)。

接口定义
1
2
3
4
public interface ConverterFactory<S, R> {
// 根据目标子类 T(继承自 R),返回对应的 Converter<S, T>
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
实战示例:String→Number 转换器工厂

实现一个工厂,支持将 String 转换为 IntegerLongDoubleNumber 子类:

步骤 1:实现 ConverterFactory 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;

public class StringToNumberConverterFactory implements ConverterFactory<String, Number> {

// 缓存已创建的转换器,避免重复创建
private final Map<Class<? extends Number>, Converter<String, ? extends Number>> converterCache = new ConcurrentHashMap<>();

@Override
@SuppressWarnings("unchecked")
public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
// 1. 从缓存获取转换器,不存在则创建
Converter<String, ? extends Number> converter = converterCache.get(targetType);
if (converter == null) {
converter = createConverter(targetType);
converterCache.put(targetType, converter);
}
// 2. 强制转换为目标转换器类型
return (Converter<String, T>) converter;
}

// 根据目标类型创建对应的转换器
private <T extends Number> Converter<String, T> createConverter(Class<T> targetType) {
return source -> {
if (source == null || source.trim().isEmpty()) {
return null;
}
// 3. 按目标类型转换字符串
if (targetType == Integer.class) {
return targetType.cast(Integer.parseInt(source));
} else if (targetType == Long.class) {
return targetType.cast(Long.parseLong(source));
} else if (targetType == Double.class) {
return targetType.cast(Double.parseDouble(source));
} else {
throw new IllegalArgumentException("不支持的目标类型:" + targetType.getName());
}
};
}
}
步骤 2:注册转换器工厂
1
2
3
4
5
6
7
8
9
// 在 WebConfig 的 conversionService() 方法中注册
@Bean
public ConversionService conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
// 添加转换器工厂(无需单独注册每个 Number 子类的转换器)
factoryBean.setConverters(Set.of(new StringToNumberConverterFactory()));
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
步骤 3:测试
1
2
3
4
5
6
7
8
9
@GetMapping("/testNumber")
public String testNumber(
@RequestParam("intVal") Integer intVal,
@RequestParam("longVal") Long longVal,
@RequestParam("doubleVal") Double doubleVal) {
return String.format("Integer: %d, Long: %d, Double: %f", intVal, longVal, doubleVal);
}
// 请求示例:http://localhost:8080/testNumber?intVal=10&longVal=20&doubleVal=30.5
// 响应:Integer: 10, Long: 20, Double: 30.500000

方式 3:实现 GenericConverter 接口(上下文感知转换)

GenericConverter 是最灵活的转换器接口,支持 “根据源类型和目标类型的上下文信息(如字段注解、泛型类型)进行转换”,适合复杂场景(如根据字段上的 @Format 注解转换日期格式、根据泛型类型转换 List<String>List<User>)。

接口定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface GenericConverter {
// 1. 返回当前转换器支持的“源类型-目标类型”对(可支持多对多)
Set<ConvertiblePair> getConvertibleTypes();

// 2. 执行转换,参数包含源对象和类型上下文(TypeDescriptor)
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

// 封装“源类型-目标类型”对
public final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
// 省略构造器、getter...
}
实战示例:基于注解的日期转换

实现一个转换器,根据字段上的 @DateTimeFormat 注解指定的格式,将 String 转换为 LocalDateTime

步骤 1:实现 GenericConverter 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Set;

public class StringToLocalDateTimeConverter implements GenericConverter {

// 1. 声明支持的转换类型:String → LocalDateTime
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> pairs = new HashSet<>();
pairs.add(new ConvertiblePair(String.class, LocalDateTime.class));
return pairs;
}

// 2. 执行转换,根据目标字段的 @DateTimeFormat 注解确定格式
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}

String dateStr = (String) source;
// 3. 获取目标字段上的 @DateTimeFormat 注解
DateTimeFormat dateTimeFormat = targetType.getAnnotation(DateTimeFormat.class);
if (dateTimeFormat == null) {
throw new ConversionFailedException(sourceType, targetType, source,
new IllegalArgumentException("目标字段需添加 @DateTimeFormat 注解"));
}

try {
// 4. 使用注解指定的格式转换日期
String pattern = dateTimeFormat.pattern();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
return LocalDateTime.parse(dateStr, formatter);
} catch (Exception e) {
throw new ConversionFailedException(sourceType, targetType, source,
new IllegalArgumentException("日期格式错误,需满足:" + dateTimeFormat.pattern(), e));
}
}
}
步骤 2:使用注解标注目标字段
1
2
3
4
5
6
7
8
9
public class Order {
private Long id;

// 标注日期格式:yyyy-MM-dd HH:mm:ss
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

// getter/setter + toString()
}
步骤 3:测试
1
2
3
4
5
6
7
@PostMapping("/testOrder")
public String testOrder(@RequestBody Order order) {
System.out.println("转换后的 Order:" + order);
// 输入 JSON:{"id":1,"createTime":"2024-05-20 14:30:00"}
// 输出:Order(id=1, createTime=2024-05-20T14:30)
return "转换成功:" + order;
}

数据绑定与校验结合

数据绑定通常与数据校验配合使用,通过 @Valid 注解触发校验,BindingResult 获取错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

// 自定义 DTO,添加校验注解
public class UserDTO {
@NotNull(message = "用户ID不能为空")
private Integer id;

@NotBlank(message = "用户名不能为空")
private String name;

// getter/setter
}

@RestController
public class UserController {

// @Validated 触发校验,BindingResult 接收校验结果
@PostMapping("/addUser")
public String addUser(@Validated @RequestBody UserDTO userDTO, BindingResult bindingResult) {
// 1. 判断是否有校验错误
if (bindingResult.hasErrors()) {
// 2. 提取错误信息并返回
String errorMsg = bindingResult.getFieldErrors().stream()
.map(error -> error.getField() + ":" + error.getDefaultMessage())
.reduce((a, b) -> a + ";" + b)
.orElse("参数校验失败");
return "错误:" + errorMsg;
}

// 3. 校验通过,执行业务逻辑
return "添加用户成功:" + userDTO;
}
}
  • 依赖要求:需添加 JSR-380 校验依赖(如 Hibernate Validator):

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
    </dependency>

总结

Spring MVC 数据绑定是简化参数处理的核心机制,其核心价值在于:

  1. 自动类型转换:通过 ConversionService 统一管理内置和自定义转换器,避免手动转换的重复代码;
  2. 灵活扩展:支持 3 种自定义转换器,满足从简单到复杂的转换需求;
  3. 校验集成:与 JSR-380 校验标准无缝结合,确保参数合法性

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10