Formatter
Spring 用于类型转换的接口:
-
Printer:将其它类型转换为字符串
-
Parser:将字符串转换为其它类型
-
Formatter:同时具备 Printer 和 Parser 的功能
-
Converter:将一种类型 S 转换为另一种类型 T(不限制类型的类型转换)
-
Converters:将上述类型转换接口封装成标准的GenericConverter 接口,并组成集合使用
-
ConversionService:转换服务,通过 Converters 完成转换
PropertyEditor
Spring 同时支持 JDK 定义的类型转换接口:
-
PropertyEditor:支持 String 与其它类型的互相转换
-
PropertyEditorRegistry:可以注册多个 PropertyEditor 实现以支持多种形式的转换
Spring 提供适配器 FormatterPropertyEditorAdapter,可以将 Formatter 类型的转换接口适配为标准的 PropertyEditor 接口:
TypeConverter
Spring 框架不会直接使用 Formatter 接口进行类型转换,做了进一步的封装:
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
需要填充属性的类:
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
要被填充属性的类:
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
要被填充属性的类:
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 方法:
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");
public Date parse(String text, Locale locale) throws ParseException {
return sdf.parse(text);
}
public String print(Date object, Locale locale) {
return sdf.format(object);
}
}
通过控制器的 InitBinder 方法扩展数据绑定:
private static class TestController{
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.
文章评论