红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 11:类型转换

2025年7月2日 8点热度 0人点赞 0条评论

Formatter

Spring 用于类型转换的接口:

image-20250630103904706

  • Printer:将其它类型转换为字符串

  • Parser:将字符串转换为其它类型

  • Formatter:同时具备 Printer 和 Parser 的功能

  • Converter:将一种类型 S 转换为另一种类型 T(不限制类型的类型转换)

  • Converters:将上述类型转换接口封装成标准的GenericConverter 接口,并组成集合使用

  • ConversionService:转换服务,通过 Converters 完成转换

PropertyEditor

Spring 同时支持 JDK 定义的类型转换接口:

image-20250630104415273

  • PropertyEditor:支持 String 与其它类型的互相转换

  • PropertyEditorRegistry:可以注册多个 PropertyEditor 实现以支持多种形式的转换

Spring 提供适配器 FormatterPropertyEditorAdapter,可以将 Formatter 类型的转换接口适配为标准的 PropertyEditor 接口:

image-20250630104914008

TypeConverter

Spring 框架不会直接使用 Formatter 接口进行类型转换,做了进一步的封装:

image-20250630105356500

TypeConverter 接口用于高层次的类型转换,有四个实现:

  • SImpleTypeConverter:仅提供基本的类型转换功能

  • BeanWrapperImpl:填充 Bean 属性(通过 Setter 方法),如果需要,则进行类型转换

  • DirectFieldAccessor:填充 Bean 属性(通过字段),如果需要,则进行类型转换

  • ServletRequestDataBinder:进行 Servlet 请求相关的数据绑定,此外还支持数据校验

这些实现底层都通过 ConversionService 和 PropertyEditorRegistry 实现类型转换,但并不直接操作这两种不同的转换服务接口,而是操作其上的封装TypeConvertDelegate,这是设计模式中门面模式(外观模式)的一种体现。

SimpleTypeConverter

利用 SimpleTypeConverter 实现类型转换:

SimpleTypeConverter converter = new SimpleTypeConverter();
Integer intVal = converter.convertIfNecessary("10", Integer.class);
Date date = converter.convertIfNecessary("2025/06/30", Date.class);
System.out.println(intVal);
System.out.println(date);

默认的转换器中不包含对2025-06-30这种格式日期字符串的转换。

BeanWrapperImpl

需要填充属性的类:

@Setter
@ToString
private static class User{
    private String name;
    private int age;
    private Date birthday;
}

使用 BeanWrapperImpl 填充数属性,并完成类型转换:

User user = new User();
BeanWrapperImpl beanWrapper = new BeanWrapperImpl(user);
beanWrapper.setPropertyValue("name","Tom");
beanWrapper.setPropertyValue("age","20");
beanWrapper.setPropertyValue("birthday","2000/1/1");
System.out.println(user);

打印:

BeanWrapperImplTests.User(name=Tom, age=20, birthday=Sat Jan 01 00:00:00 CST 2000)

DirectFieldAccessor

要被填充属性的类:

@ToString
private static class User{
    private String name;
    private int age;
    private Date birthday;
}

进行类型转换并填充属性:

User user = new User();
DirectFieldAccessor dfa = new DirectFieldAccessor(user);
dfa.setPropertyValue("name","Tom");
dfa.setPropertyValue("age","18");
dfa.setPropertyValue("birthday","2000/1/1");
System.out.println(user);

与BeanWrapperImpl不同,DirectFieldAccessor是通过反射直接给字段赋值,因此不需要使用 Setter 方法,所以在这个示例中User没有使用@Setter注解。

DataBinder

要被填充属性的类:

@Setter
@ToString
private static class User{
    private String name;
    private int age;
    private Date birthday;
}

使用 DataBInder 填充属性:

User user = new User();
DataBinder dataBinder = new DataBinder(user);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "Tom");
pvs.add("age", "20");
pvs.add("birthday", "2000/1/1");
dataBinder.bind(pvs);
System.out.println(user);

默认情况下 DataBinder 通过 Setter 完成属性填充,如果没有 Setter 方法:

@ToString
private static class User{
    private String name;
    private int age;
    private Date birthday;
}

结果:

DataBinderTests2.User(name=null, age=0, birthday=null)

没有正常填充属性,且没有报错信息。

可以通过initDirectFieldAccess方法开启开关,让 DataBinder 通过字段反射完成属性填充:

User user = new User();
DataBinder dataBinder = new DataBinder(user);
dataBinder.initDirectFieldAccess();
// ...

dataBinder.initDirectFieldAccess()需要在dataBinder.bind(pvs)方法前调用,否则会报错。

ServletRequestDataBinder

如果是 Web 环境,可能需要从请求对象中的参数绑定数据,可以:

User user = new User();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(user, "user");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("name", "Tom");
request.addParameter("age", "20");
request.addParameter("birthday", "2000/1/1");
ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(request);
dataBinder.bind(pvs);
System.out.println(user);

ServletRequestDataBinder是DataBinder的一个子类,ServletRequestParameterPropertyValues是MutablePropertyValues的一个子类。

除了通过ServletRequestParameterPropertyValues绑定数据,可以直接从 Request 对象绑定数据:

User user = new User();
ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(user, "user");
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("name", "Tom");
request.addParameter("age", "20");
request.addParameter("birthday", "2000/1/1");
dataBinder.bind(request);
System.out.println(user);

扩展 DataBinder

可以通过添加自定义类型转换器的方式扩展 DataBinder,以实现对更多类型的支持。

ServletRequestDataBinderFactory

使用数据绑定工厂创建数据绑定:

User user = new User();
List<InvocableHandlerMethod> methods = new ArrayList<>();
ServletRequestDataBinderFactory dataBinderFactory = new ServletRequestDataBinderFactory(methods,null);
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter("name", "Tom");
request.addParameter("age", "20");
request.addParameter("birthday", "2000/1/1");
WebDataBinder webDataBinder = dataBinderFactory.createBinder(new ServletWebRequest(request), user, "user");
webDataBinder.bind(new ServletRequestParameterPropertyValues(request));
System.out.println(user);

InitBinder 方法

添加自定义类型转换器:

private static class MyDateFormatter implements Formatter<Date> {
​
    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
​
    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        return sdf.parse(text);
    }
​
    @Override
    public String print(Date object, Locale locale) {
        return sdf.format(object);
    }
}

通过控制器的 InitBinder 方法扩展数据绑定:

@Controller
private static class TestController{
    @InitBinder
    public void initBinder(WebDataBinder binder){
        binder.addCustomFormatter(new MyDateFormatter());
    }
}

InitBinder 方法由@InitBinder注解标记,且参数类型为WebDataBinder。

使用 InitBinder 方法构造数据绑定工厂:

Method initBinderMethod = TestController.class.getMethod("initBinder", WebDataBinder.class);
List<InvocableHandlerMethod> methods = List.of(new InvocableHandlerMethod(new TestController(), initBinderMethod));
ServletRequestDataBinderFactory dataBinderFactory = new ServletRequestDataBinderFactory(methods,null);
// ...

现在,数据绑定工厂创建的DataBinder对象也可以处理2000-01-01这样的非标准格式的日期了。

ConversionService

可以使用ConversionService扩展 DataBinding:

ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer();
FormattingConversionService conversionService = new FormattingConversionService();
conversionService.addFormatter(new MyDateFormatter());
webBindingInitializer.setConversionService(conversionService);
ServletRequestDataBinderFactory dataBinderFactory = new ServletRequestDataBinderFactory(null, webBindingInitializer);
// ...

如果同时使用两种方式扩展:

// 通过 initBinder方法扩展
Method initBinderMethod = TestController.class.getMethod("initBinder", WebDataBinder.class);
List<InvocableHandlerMethod> methods = List.of(new InvocableHandlerMethod(new TestController(), initBinderMethod));
// 通过 conversion service 扩展
ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer();
FormattingConversionService conversionService = new FormattingConversionService();
conversionService.addFormatter(new MyDateFormatter("通过 conversion service 扩展"));
webBindingInitializer.setConversionService(conversionService);
ServletRequestDataBinderFactory dataBinderFactory = new ServletRequestDataBinderFactory(methods, webBindingInitializer);

通过initBinder 添加的类型转换器优先级更高,因此这里实际调用的是通过 InitBinder 方法添加的转换器。

默认转换器

DefaultFormattingConversionService中包含默认的转换器,可以用它提供一些通用的类型转换支持:

// 使用默认的转换器
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
webBindingInitializer.setConversionService(conversionService);
ServletRequestDataBinderFactory dataBinderFactory = new ServletRequestDataBinderFactory(null, webBindingInitializer);

对于日期转换,需要搭配注解使用:

@Setter
@ToString
private static class User2 {
    private String name;
    private int age;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
}

如果是 SpringBoot 应用,还可以使用ApplicationConversionService:

ApplicationConversionService conversionService = new ApplicationConversionService();
webBindingInitializer.setConversionService(conversionService);

获取泛型参数

存在这样的继承关系:

private static class Parent<T>{}
private static class Child extends Parent<String>{}

父类有泛型参数,子类继承的时候使用具体类型String。如果要获取某个类继承父类时泛型参数使用的具体类型,可以通过反射获取:

Type genericSuperclass = Child.class.getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType parameterizedType) {
    Type type = parameterizedType.getActualTypeArguments()[0];
    System.out.println(type);
}

需要注意的是,getSuperclass方法同样可以获取到父类的类型,但该类型不包含泛型参数信息。getGenericSuperclass返回的父类型是Type,该类型也可能是不包含泛型的普通父类,因此需要将其转换为表示带泛型参数的子类型ParameterizedType。该类型的getActualTypeArguments方法返回实际使用的泛型参数数组(可以有多个泛型参数)。

Spring 也具有相同功能的 API:

Class<?> clazz = GenericTypeResolver.resolveTypeArgument(Child.class, Parent.class);
System.out.println(clazz);

resolveTypeArgument方法的第一个参数是基类的类对象,第二个参数是包含泛型参数的父类的类对象。

如果有多个泛型参数:

Class<?>[] classes = GenericTypeResolver.resolveTypeArguments(Child.class, Parent.class);

本文的完整示例代码可以从这里获取。

The End.

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: spring boot 数据绑定 泛型参数 类型转换
最后更新:2025年7月2日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号