对象绑定与类型转换

类图

底层第一套转换接口与实现
classDiagram Formatter --|> Printer Formatter --|> Parser class Converters { Set~GenericConverter~ } class Converter class ConversionService class FormattingConversionService ConversionService <|-- FormattingConversionService FormattingConversionService o-- Converters Printer --> Adapter1 Adapter1 --> Converters Parser --> Adapter2 Adapter2 --> Converters Converter --> Adapter3 Adapter3 --> Converters <<interface>> Formatter <<interface>> Printer <<interface>> Parser <<interface>> Converter <<interface>> ConversionService
  • Printer 把其它类型转为 String
  • Parser 把 String 转为其它类型
  • Formatter 综合 Printer 与 Parser 功能
  • Converter 把类型 S 转为类型 T
  • Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
  • FormattingConversionService 利用其它们实现转换
 
底层第二套转换接口
classDiagram PropertyEditorRegistry o-- "多" PropertyEditor <<interface>> PropertyEditorRegistry <<interface>> PropertyEditor
  • PropertyEditor 把 String 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配
 
高层接口与实现
classDiagram TypeConverter <|-- SimpleTypeConverter TypeConverter <|-- BeanWrapperImpl TypeConverter <|-- DirectFieldAccessor TypeConverter <|-- ServletRequestDataBinder SimpleTypeConverter --> TypeConverterDelegate BeanWrapperImpl --> TypeConverterDelegate DirectFieldAccessor --> TypeConverterDelegate ServletRequestDataBinder --> TypeConverterDelegate TypeConverterDelegate --> ConversionService TypeConverterDelegate --> PropertyEditorRegistry <<interface>> TypeConverter <<interface>> ConversionService <<interface>> PropertyEditorRegistry
  • 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式)
    • 首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor)
    • 再看有没有 ConversionService 转换
    • 再利用默认的 PropertyEditor 转换
    • 最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property
  • DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
  • ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能

案例

非web环境

SimpleConverter

public class TestSimpleConverter { public static void main(String[] args) { // 仅有类型转换的功能 SimpleTypeConverter typeConverter = new SimpleTypeConverter(); Integer number = typeConverter.convertIfNecessary("13", int.class); Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class); System.out.println(number); System.out.println(date); } }
输出
13 Thu Mar 04 00:00:00 CST 1999

BeanWrapper

public class TestBeanWrapper { public static void main(String[] args) { // 利用反射原理, 为 bean 的属性赋值 MyBean target = new MyBean(); BeanWrapperImpl wrapper = new BeanWrapperImpl(target); wrapper.setPropertyValue("a", "10"); wrapper.setPropertyValue("b", "hello"); wrapper.setPropertyValue("c", "1999/03/04"); System.out.println(target); } static class MyBean { private int a; private String b; private Date c; public int getA() { return a; } public void setA(int a) { this.a = a; } public String getB() { return b; } public void setB(String b) { this.b = b; } public Date getC() { return c; } public void setC(Date c) { this.c = c; } @Override public String toString() { return "MyBean{" + "a=" + a + ", b='" + b + '\'' + ", c=" + c + '}'; } } }
输出
MyBean{a=10, b='hello', c=Thu Mar 04 00:00:00 CST 1999}

FieldAccessor

public class TestFieldAccessor { public static void main(String[] args) { // 利用反射原理, 为 bean 的属性赋值 MyBean target = new MyBean(); DirectFieldAccessor accessor = new DirectFieldAccessor(target); accessor.setPropertyValue("a", "10"); accessor.setPropertyValue("b", "hello"); accessor.setPropertyValue("c", "1999/03/04"); System.out.println(target); } static class MyBean { private int a; private String b; private Date c; @Override public String toString() { return "MyBean{" + "a=" + a + ", b='" + b + '\'' + ", c=" + c + '}'; } } }
MyBean{a=10, b='hello', c=Thu Mar 04 00:00:00 CST 1999}

DataBinder

默认走set方法绑定数据
public class TestDataBinder { public static void main(String[] args) { // 执行数据绑定 MyBean target = new MyBean(); DataBinder dataBinder = new DataBinder(target); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("a", "10"); pvs.add("b", "hello"); pvs.add("c", "1999/03/04"); dataBinder.bind(pvs); System.out.println(target); } static class MyBean { private int a; private String b; private Date c; public int getA() { return a; } public void setA(int a) { this.a = a; } public String getB() { return b; } public void setB(String b) { this.b = b; } public Date getC() { return c; } public void setC(Date c) { this.c = c; } @Override public String toString() { return "MyBean{" + "a=" + a + ", b='" + b + '\'' + ", c=" + c + '}'; } } }
 
MyBean{a=10, b='hello', c=Thu Mar 04 00:00:00 CST 1999}
去掉set方法后
static class MyBean { private int a; private String b; private Date c; @Override public String toString() { return "MyBean{" + "a=" + a + ", b='" + b + '\'' + ", c=" + c + '}'; } }
 
MyBean{a=0, b='null', c=null}
dataBinder.initDirectFieldAccess(); 开启直接字段的访问
MyBean{a=10, b='hello', c=Thu Mar 04 00:00:00 CST 1999}

web环境

ServletRequestDataBinder

public class TestServletDataBinder { public static void main(String[] args) { // web 环境下数据绑定 MyBean target = new MyBean(); ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target); MockHttpServletRequest request = new MockHttpServletRequest(); request.setParameter("a", "10"); request.setParameter("b", "hello"); request.setParameter("c", "1999/03/04"); dataBinder.bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target); } static class MyBean { private int a; private String b; private Date c; public int getA() { return a; } public void setA(int a) { this.a = a; } public String getB() { return b; } public void setB(String b) { this.b = b; } public Date getC() { return c; } public void setC(Date c) { this.c = c; } @Override public String toString() { return "MyBean{" + "a=" + a + ", b='" + b + '\'' + ", c=" + c + '}'; } } }
 
MyBean{a=10, b='hello', c=Thu Mar 04 00:00:00 CST 1999}

数据绑定工厂

ServletRequestDataBinder 的局限性

使用之前的ServletRequestDataBinder 去绑定参数无法支持特殊的数据类型,比如日期,如果输入的是自定义格式YYYY|MM|DD
TestServletDataBinderFactory { public static void main(String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setParameter("birthday", "1999|01|02"); request.setParameter("address.name", "西安"); User target = new User(); ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target, "user"); dataBinder.bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target); } public static class User { @DateTimeFormat(pattern = "yyyy|MM|dd") private Date birthday; private Address address; public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "User{" + "birthday=" + birthday + ", address=" + address + '}'; } } public static class Address { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Address{" + "name='" + name + '\'' + '}'; } } }
输出
User{birthday=null, address=Address{name='西安'}}
日期类型没有绑定上
 
使用工厂创建转换器
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null); factory.createBinder(new ServletWebRequest(request), target, "user") .bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target);
 
User{birthday=null, address=Address{name='西安'}}

用 @InitBinder 转换

static class MyController { @InitBinder public void aaa(WebDataBinder dataBinder) { // 扩展 dataBinder 的转换器 dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的")); } }
自定义转换器,继承Formatter接口
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.format.Formatter; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class MyDateFormatter implements Formatter<Date> { private static final Logger log = LoggerFactory.getLogger(MyDateFormatter.class); private final String desc; public MyDateFormatter(String desc) { this.desc = desc; } @Override public String print(Date date, Locale locale) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd"); return sdf.format(date); } @Override public Date parse(String text, Locale locale) throws ParseException { log.debug(">>>>>> 进入了: {}", desc); SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd"); return sdf.parse(text); } }
 
public static void main(String[] args) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setParameter("birthday", "1999|01|02"); request.setParameter("address.name", "西安"); User target = new User(); InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class)); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), null); factory.createBinder(new ServletWebRequest(request), target, "user") .bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target); }
 
[DEBUG] 19:33:47.123 [main] com.onethink.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 方式扩展的 User{birthday=Sat Jan 02 00:00:00 CST 1999, address=Address{name='西安'}}

ConversionService

 
FormattingConversionService service = new FormattingConversionService(); service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能")); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer); factory.createBinder(new ServletWebRequest(request), target, "user") .bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target);
[DEBUG] 19:39:23.555 [main] com.onethink.a23.MyDateFormatter - >>>>>> 进入了: 用 ConversionService 方式扩展转换功能 User{birthday=Sat Jan 02 00:00:00 CST 1999, address=Address{name='西安'}}
 
同时使用ConversionService和@initBinder
@initBinder优先级更高
 
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class)); FormattingConversionService service = new FormattingConversionService(); service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能")); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(List.of(method), initializer); // "5. 使用默认 ConversionService 转换" WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user"); dataBinder.bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target);
[DEBUG] 19:44:19.667 [main] com.onethink.a23.MyDateFormatter - >>>>>> 进入了: 用 @InitBinder 方式扩展的 User{birthday=Sat Jan 02 00:00:00 CST 1999, address=Address{name='西安'}}
 
使用默认的ConversionService 不需要自定义formatter,配合 @DateTimeFormat(pattern = "yyyy|MM|dd")
springboot使用ApplicationConversionService
非springboot用DefaultConversionService
ApplicationConversionService service = new ApplicationConversionService(); ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer(); initializer.setConversionService(service); ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user"); dataBinder.bind(new ServletRequestParameterPropertyValues(request)); System.out.println(target);
 
User{birthday=Sat Jan 02 00:00:00 CST 1999, address=Address{name='西安'}}

泛型参数获取

class BaseDao<T> { T findOne() { return null; } } class TeacherDao extends BaseDao<Teacher> { }
 
public class TestGenericType { public static void main(String[] args) { // 小技巧 // 1. java api System.out.println(">>>>>>>>>>>>>>>>>>>>>>>"); Type type = TeacherDao.class.getGenericSuperclass(); System.out.println(type); if (type instanceof ParameterizedType parameterizedType) { System.out.println(parameterizedType.getActualTypeArguments()[0]); } // 2. spring api 1 单个泛型参数 System.out.println(">>>>>>>>>>>>>>>>>>>>>>>"); Class<?> t = GenericTypeResolver.resolveTypeArgument(TeacherDao.class, BaseDao.class); System.out.println(t); // 3. spring api 2 System.out.println(">>>>>>>>>>>>>>>>>>>>>>>"); System.out.println(ResolvableType.forClass(TeacherDao.class).getSuperType().getGeneric().resolve()); } }