👋 前言
前面我们已经玩转了 Spring 核心、Web 流程、配置体系与事件驱动:IOC → 生命周期 → 循环依赖 → AOP → 事务 → SpringMVC → Boot 自动配置 → 资源加载 → 事件驱动。
接下来,我们聚焦一个日常开发中高频出现却容易被忽略的细节:数据绑定 。我们在 Controller 中写的 @RequestParam String id 如何自动转为 Long?@Valid 注解如何实现参数校验?前端传的字符串如何绑定到实体类的日期、枚举字段?
本篇基于 Spring 5.3.x,系统拆解 类型转换与校验体系:
- Converter、ConversionService 核心类型转换机制;
- Formatter、PropertyEditor 传统类型绑定的区别与用法;
- @Valid、Validator 校验原理,参数校验如何生效;
- SpringMVC 参数自动绑定的底层逻辑;
- 自定义类型转换器、自定义校验器的实战实现。
补上 "数据绑定" 的最后一块拼图,让你彻底清楚:前端传入的字符串,如何一步步变成业务代码中可用的 Java 类型。
一、核心基础:类型转换体系(Converter 与 ConversionService)
Spring 为了解决 不同类型之间的转换问题 (如 String → Long、String → Date、String → 枚举),提供了一套统一的类型转换体系。其核心是 Converter 接口 和 ConversionService 接口 ,它们替代了 Java 原生的 PropertyEditor(存在局限性),是 Spring 类型转换的现代基石。
1. 核心接口:Converter(最基础的类型转换器)
Converter<S, T> 是一个函数式接口,负责将 源类型 S 转换为 目标类型 T,是所有类型转换器的基础。
源码(Spring 5.3.x)
java
@FunctionalInterface
public interface Converter<S, T> {
// 核心方法:将源类型 S 转换为目标类型 T
@Nullable
T convert(S source);
}
Spring 的类型转换 SPI 包含四个层次的转换器接口:
| 转换器类型 | 适用场景 | 示例 |
|---|---|---|
| Converter<S, T> | 单一源类型 → 单一目标类型的转换 | 实现 convert(S) 方法,无条件执行 |
| GenericConverter | 支持多组源类型和目标类型之间的转换 | 返回 Set<ConvertiblePair>,可处理一对多、多对多 |
| ConditionalConverter | 提供条件判断能力 | 实现 matches(TypeDescriptor, TypeDescriptor) 方法 |
| ConditionalGenericConverter | 具备条件判断的通用转换器 | = GenericConverter + ConditionalConverter |
Converter 是最基础的转换器接口,适用于简单的 1:1 转换场景;当需要更灵活的功能时,可以根据需求选择合适的 SPI 接口。
自定义 Converter 实战(简单易实现)
如果内置转换器不满足需求(如 String → 自定义实体类),可手动实现:
java
// 示例:将 String(格式:name,age)转换为 User 实体
public class StringToUserConverter implements Converter<String, User> {
@Override
public User convert(String source) {
if (source == null || source.isEmpty()) {
return null;
}
String[] parts = source.split(",");
return new User(parts[0], Integer.parseInt(parts[1]));
}
}
2. 核心接口:ConversionService(转换器的统一管理)
Converter 是单个转换器,而 ConversionService 是转换器的容器 ,负责管理所有 Converter,提供统一的类型转换入口,是 Spring 类型转换的核心入口。
核心源码(Spring 5.3.x)
java
public interface ConversionService {
// 1. 判断是否能将源类型 S 转换为目标类型 T
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
// 2. 核心:执行类型转换
@Nullable
<T> T convert(@Nullable Object source, Class<T> targetType);
// 重载方法:指定源类型和目标类型(更精准)
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
核心实现类:DefaultConversionService
Spring 默认使用 DefaultConversionService,它会 自动注册所有内置转换器 ,同时支持手动添加自定义转换器,是日常开发中最常用的 ConversionService 实现。
java
// 示例:创建 ConversionService 并添加自定义转换器
ConversionService conversionService = new DefaultConversionService();
((DefaultConversionService) conversionService).addConverter(new StringToUserConverter());
// 执行转换
User user = conversionService.convert("zhangsan,20", User.class);
3. 核心结论
- Converter:单个类型转换的实现,负责具体的转换逻辑;
- ConversionService:转换器的统一管理容器,提供统一的转换入口;
- Spring 容器启动时,会自动创建
DefaultConversionService,并注册所有内置转换器。
二、传统类型绑定:Formatter 与 PropertyEditor
除了 Converter 体系,Spring 还提供了 Formatter 和 PropertyEditor 用于类型转换,二者有明确的使用场景区别,核心用于 页面参数绑定、配置文件参数绑定。
1. Formatter(格式化转换器,面向用户输入)
Formatter<T> 专门用于 字符串与目标类型的双向转换(如页面输入的字符串 → Java 类型,Java 类型 → 页面显示的字符串),常用于 Web 场景(SpringMVC 参数绑定)。
核心源码
java
public interface Formatter<T> {
// 1. 字符串 → 目标类型 T(页面输入 → Java 类型)
T parse(String text, Locale locale) throws ParseException;
// 2. 目标类型 T → 字符串(Java 类型 → 页面显示)
String print(T object, Locale locale);
}
与 Converter 的区别
| 特性 | Converter<S, T> | Formatter |
|---|---|---|
| 转换方向 | 从源类型到目标类型的单向转换(S → T) | 双向(String ↔ T) |
| 适用场景 | 任意类型之间转换 | 字符串与 Java 类型转换(Web 场景) |
| 底层机制 | 直接实现转换逻辑 | 通过 FormattingConversionService 适配为 Converter |
| 依赖 Locale | 不依赖 | 依赖(支持国际化格式化,如日期、数字) |
常用内置 Formatter
NumberFormatter:数字与字符串双向转换(支持国际化)DateFormatter:日期与字符串双向转换(支持自定义格式)
2. PropertyEditor(Java 原生,Spring 兼容)
PropertyEditor 是 JavaBeans 规范定义的接口,通过 setAsText(String) 和 getAsText() 也支持双向转换(String ↔ 对象)。Spring 的 DataBinder 和 BeanWrapper 底层长期依赖 PropertyEditor 进行属性解析和类型转换。
定位 :Spring 3.0 引入 ConversionService 体系作为更现代、类型安全且支持泛型的替代方案。Spring 5.x 中仍保留 PropertyEditor 体系作为兼容方案,但在新代码中推荐使用 Converter/Formatter 体系。
核心源码(Java 原生)
java
public interface PropertyEditor {
// 核心:将字符串转换为目标类型
void setAsText(String text) throws IllegalArgumentException;
// 获取转换后的目标对象
Object getValue();
// 将目标对象转换为字符串(用于显示)
String getAsText();
}
Spring 扩展:PropertyEditorRegistrar
Spring 提供 PropertyEditorRegistrar,用于注册自定义 PropertyEditor,适配传统配置场景:
java
public interface PropertyEditorRegistrar {
// 注册自定义 PropertyEditor 到 PropertyEditorRegistry
void registerCustomEditors(PropertyEditorRegistry registry);
}
3. 核心结论
- Formatter:Spring 推荐,双向转换,支持国际化,适用于 Web 场景;
- PropertyEditor:Java 原生,双向转换,适用于传统配置场景,Spring 仅做兼容,不推荐新代码使用。
三、SpringMVC 参数自动绑定底层
我们在 SpringMVC 中写的 Controller 方法,如:
java
@GetMapping("/user")
public String getUser(@RequestParam Long id, @ModelAttribute User user) {
// ...
}
前端传入的 字符串参数 (id=123、user.name=zhangsan),能自动转为 Long、User 类型,底层就是 类型转换体系 在工作。结合 SpringMVC 的流程,完整底层逻辑如下:
核心流程(SpringMVC 参数绑定 + 类型转换)
1. 前端发送请求(参数为字符串)
↓
2. DispatcherServlet 接收请求,找到对应的 HandlerMethod;
2.1 通过 HandlerMethodArgumentResolver(参数解析器)判断参数类型,选择合适的解析器实现:
2.2 RequestParamMethodArgumentResolver:解析 @RequestParam 参数
2.3 ServletModelAttributeMethodProcessor:解析 @ModelAttribute 参数
2.4 RequestResponseBodyMethodProcessor:解析 @RequestBody 参数
3. 解析器获取请求参数后,委托给 WebDataBinder 进行数据绑定和类型转换;
4. WebDataBinder 底层调用 ConversionService(或 Formatter)将字符串参数转换为目标类型;
5. 转换完成后,通过反射调用 Controller 方法,完成参数注入。
关键源码(HandlerMethodArgumentResolver 调用转换)
以 RequestParamMethodArgumentResolver(解析 @RequestParam 参数)为例:
java
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 1. 从请求中获取字符串参数
String[] paramValues = webRequest.getParameterValues(name);
String paramValue = StringUtils.arrayToCommaDelimitedString(paramValues);
// 2. 获取 ConversionService
ConversionService conversionService = mavContainer.getConversionService();
// 3. 执行类型转换(字符串 → 目标类型,如 String → Long)
return conversionService.convert(paramValue, parameter.getParameterType());
}
核心结论
SpringMVC 参数自动绑定的核心,是 HandlerMethodArgumentResolver + ConversionService/Formatter:解析器负责获取请求参数,转换器负责将字符串参数转为目标类型,最终完成参数注入。
四、数据校验体系:@Valid 与 Validator
日常开发中,我们常用 @Valid 或 @Validated 注解校验请求参数(如实体类的字段长度、格式),底层依赖 Spring 的校验体系,核心是 Validator 接口 ,适配 JSR-380 规范(如 @NotNull、@NotBlank、@Min 等注解)。
1. 核心接口:Validator(校验器)
Validator 是 Spring 校验体系的核心接口,负责对 Java Bean 进行校验,判断字段是否符合规则。
核心源码
java
public interface Validator {
// 1. 判断当前校验器是否支持校验该类型的 Bean
boolean supports(Class<?> clazz);
// 2. 核心:校验 Bean,将校验结果存入 Errors 对象
void validate(Object target, Errors errors);
}
核心实现类:LocalValidatorFactoryBean
Spring 提供 LocalValidatorFactoryBean,实现了 Validator 接口,同时适配 JSR-380 规范(如 Hibernate Validator),是日常开发中最常用的校验器实现。
2. @Valid 注解底层原理
核心流程
- 配置类添加
@EnableWebMvc(或 Spring Boot 自动配置),Spring 会自动注册LocalValidatorFactoryBean; - 在 Controller 方法的参数(实体类)前添加
@Valid,触发参数校验;@Validated是 Spring 的扩展注解,用于支持分组校验(Groups)和方法级别校验,可替代@Valid使用。
- SpringMVC 中,
RequestResponseBodyMethodProcessor(处理@RequestBody)或ModelAttributeMethodProcessor(处理@ModelAttribute),会调用Validator进行校验; - 校验失败时,抛出
MethodArgumentNotValidException,可通过全局异常处理器捕获并返回提示。
关键源码(参数校验触发)
java
// RequestResponseBodyMethodProcessor 解析 @RequestBody 参数时触发校验
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数上的 @Valid、@Validated 注解
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
// 调用 Validator 进行校验
binder.validate();
break;
}
}
}
3. 常用校验注解(JSR-380 规范)
| 注解 | 作用 |
|---|---|
@NotNull |
字段不能为 null |
@NotBlank |
字符串不能为 null 且不能为空白(空格、换行等) |
@NotEmpty |
集合不能为 null 且不能为空,字符串不能为 null 且长度 > 0 |
@Min(value) |
数字最小值 |
@Max(value) |
数字最大值 |
@Pattern(regexp) |
字符串符合指定正则表达式 |
@Email |
字符串符合邮箱格式 |
4. 自定义校验器实战
如果内置校验注解不满足需求(如自定义手机号校验),可手动实现 Validator:
java
// 1. 自定义校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) // 指定校验器
public @interface Phone {
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
// 手机号正则
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 允许为空,可配合 @NotNull 使用
}
return value.matches(PHONE_REGEX);
}
}
// 3. 使用
public class User {
@Phone(message = "手机号格式错误")
private String phone;
// getter/setter
}
五、核心总结
- 类型转换核心 :
Converter(单个转换)+ConversionService(统一管理),Spring 内置大量转换器; - 双向转换 :
Formatter(支持国际化,Web 场景),区别于Converter的单向转换; - 传统转换 :
PropertyEditor(Java 原生,双向,Spring 兼容,不推荐新用); - SpringMVC 参数绑定 :
HandlerMethodArgumentResolver结合ConversionService,完成字符串 → 目标类型转换; - 数据校验 :
Validator为核心,LocalValidatorFactoryBean为实现,适配 JSR-380 注解;- 支持
@NotNull、@NotBlank、@Min、@Max等约束注解; @Valid(JSR-303/349 标准):触发参数校验和级联校验;@Validated(Spring 扩展):支持分组校验(Groups),可标注在类级别触发方法校验。
- 支持
- 扩展方式 :自定义
Converter、自定义Formatter、自定义Validator,满足业务个性化需求。
六、高频面试题
-
Spring 类型转换体系的核心组件是什么?
答 :核心是Converter(单个类型转换实现)和ConversionService(转换器统一管理容器),DefaultConversionService是默认实现,自动注册内置转换器。 -
Converter 和 Formatter 的区别是什么?
答 :①Converter是单向转换(S→T),Formatter是双向转换(String↔T);②Converter适用于任意类型转换,Formatter仅适用于字符串与 Java 类型转换;③Formatter支持国际化,Converter不支持。 -
SpringMVC 中,前端传入的字符串参数如何自动转为 Java 类型?
答 :底层是HandlerMethodArgumentResolver(参数解析器)获取请求参数,调用ConversionService或Formatter,将字符串参数转为目标类型,最终注入到 Controller 方法中。 -
@Valid 注解的底层原理是什么?
答 :@Valid是 JSR-380 规范注解,Spring 通过LocalValidatorFactoryBean(实现Validator接口)进行校验;Controller 方法参数添加@Valid后,SpringMVC 的参数解析器会触发校验,校验失败抛出MethodArgumentNotValidException。 -
如何实现自定义类型转换 / 自定义校验?
答 :① 自定义类型转换:实现Converter接口,添加到ConversionService中;② 自定义校验:自定义校验注解,实现ConstraintValidator接口,指定注解的校验器。