示例二:使用Printer,有中间转换
基于示例一,若要实现Person -> String的话,只需再给写一个Person -> Integer的转换器放进ConversionService里即可。
说明:一般来说ConversionService已经具备很多“能力”了的,拿来就用即可。本例为了帮你说明底层原理,所以用的是一个“干净的”ConversionService实例
@Test public void test2() { FormattingConversionService formattingConversionService = new FormattingConversionService(); FormatterRegistry formatterRegistry = formattingConversionService; // 说明:这里不使用DefaultConversionService是为了避免默认注册的那些转换器对结果的“干扰”,不方便看效果 // ConversionService conversionService = new DefaultConversionService(); ConversionService conversionService = formattingConversionService; // 注册格式化器 formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), null); // 强调:此处绝不能使用lambda表达式代替,否则泛型类型丢失,结果将出错 formatterRegistry.addConverter(new Converter<Person, Integer>() { @Override public Integer convert(Person source) { return source.getId(); } }); // 最终均使用ConversionService统一提供服务转换 System.out.println(conversionService.canConvert(Person.class, String.class)); System.out.println(conversionService.convert(new Person(1, "YourBatman"), String.class)); }
运行程序,输出:
true 11
完美。
针对本例,有如下关注点:
1.使用addFormatterForFieldType()方法注册了IntegerPrinter,并且明确指定了处理的类型:只处理Person类型
1.说明:IntegerPrinter是可以注册多次分别用于处理不同类型。比如你依旧可以保留formatterRegistry.addPrinter(new IntegerPrinter());来处理Integer -> String是木问题的
2.因为IntegerPrinter 实际上 只能转换 Integer -> String,因此还必须注册一个转换器,用于Person -> Integer桥接一下,这样就串起来了Person -> Integer -> String。只是外部看起来这些都是IntegerPrinter做的一样,特别工整
3.强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错
1.若想用lambda表达式,请使用addConverter(Class,Class,Converter)这个重载方法完成注册
ParserConverter:Parser接口适配器
把Parser<?>适配为转换器,转换目标为String -> fieldType。
private static class ParserConverter implements GenericConverter { private final Class<?> fieldType; private final Parser<?> parser; private final ConversionService conversionService; ... // 省略构造器 // String -> fieldType @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); } }
既然是转换器,重点当然是它的convert转换方法:
ParserConverter: @Override @Nullable public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { // 空串当null处理 String text = (String) source; if (!StringUtils.hasText(text)) { return null; } ... Object result = this.parser.parse(text, LocaleContextHolder.getLocale()); ... // 解读/转换结果 TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); if (!resultType.isAssignableTo(targetType)) { result = this.conversionService.convert(result, resultType, targetType); } return result; }
转换步骤分为两步:
- 通过Parser将String转换为指定的类型结果result(若失败,则抛出异常)
- 判断若result属于目标类型的子类型,直接返回,否则调用ConversionService转换一把
可以看到它和Printer的“顺序”是相反的,在返回值上做文章。同样的,下面将用两个例子来加深理解
private static class IntegerParser implements Parser<Integer> { @Override public Integer parse(String text, Locale locale) throws ParseException { return NumberUtils.parseNumber(text, Integer.class); } }
示例一:使用Parser,无中间转换
书写测试用例:
@Test public void test3() { FormattingConversionService formattingConversionService = new FormattingConversionService(); FormatterRegistry formatterRegistry = formattingConversionService; ConversionService conversionService = formattingConversionService; // 注册格式化器 formatterRegistry.addParser(new IntegerParser()); System.out.println(conversionService.canConvert(String.class, Integer.class)); System.out.println(conversionService.convert("1", Integer.class)); }
运行程序,输出:
true 1
完美。
示例二:使用Parser,有中间转换
下面示例输入一个“1”字符串,出来一个Person对象(因为有了上面例子的铺垫,这里就“直抒胸臆”了哈)。
@Test public void test4() { FormattingConversionService formattingConversionService = new FormattingConversionService(); FormatterRegistry formatterRegistry = formattingConversionService; ConversionService conversionService = formattingConversionService; // 注册格式化器 formatterRegistry.addFormatterForFieldType(Person.class, null, new IntegerParser()); formatterRegistry.addConverter(new Converter<Integer, Person>() { @Override public Person convert(Integer source) { return new Person(source, "YourBatman"); } }); System.out.println(conversionService.canConvert(String.class, Person.class)); System.out.println(conversionService.convert("1", Person.class)); }
运行程序,啪,空指针了:
java.lang.NullPointerException at org.springframework.format.support.FormattingConversionService$PrinterConverter.resolvePrinterObjectType(FormattingConversionService.java:179) at org.springframework.format.support.FormattingConversionService$PrinterConverter.<init>(FormattingConversionService.java:155) at org.springframework.format.support.FormattingConversionService.addFormatterForFieldType(FormattingConversionService.java:95) at cn.yourbatman.formatter.Demo.test4(Demo.java:86) ...
根据异常栈信息,可明确原因为:addFormatterForFieldType()方法的第二个参数不能传null,否则空指针。这其实是Spring Framework的bug,我已向社区提了issue,期待能够被解决喽:
为了正常运行本例,这么改一下:
/
// 第二个参数不传null,用IntegerPrinter占位 formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), new IntegerParser()
再次运行程序,输出:
true Person(id=1, name=YourBatman)
完美。
针对本例,有如下关注点:
- 使用addFormatterForFieldType()方法注册了IntegerParser,并且明确指定了处理的类型,用于处理Person类型
- 也就是说此IntegerParser专门用于转换目标类型为Person的属性
- 因为IntegerParser 实际上 只能转换 String -> Integer,因此还必须注册一个转换器,用于Integer -> Person桥接一下,这样就串起来了String -> Integer -> Person。外面看起来这些都是IntegerParser做的一样,非常工整
- 同样强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,否则会失去泛型类型,导致出错
二者均持有ConversionService带来哪些增强?
说明:关于如此重要的ConversionService你懂的,遗忘了的可乘坐电梯到这复习
对于PrinterConverter和ParserConverter来讲,它们的源目的是实现 String <-> Object,特点是:
- PrinterConverter:出口必须是String类型,入口类型也已确定,即Printer<T>的泛型类型,只能处理 T(或T的子类型) -> String
- ParserConverter:入口必须是String类型,出口类型也已确定,即Parser<T>的泛型类型,只能处理 String -> T(或T的子类型)
按既定“规则”,它俩的能力范围还是蛮受限的。Spring厉害的地方就在于此,可以巧妙的通过组合的方式,扩大现有组件的能力边界。比如本利中它就在PrinterConverter/ParserConverter里分别放入了ConversionService引用,从而到这样的效果:
通过能力组合协作,起到串联作用,从而扩大输入/输出“范围”,感觉就像起到了放大镜的效果一样,这个设计还是很讨巧的。
✍总结
本文以介绍FormatterRegistry接口为中心,重点研究了此接口的实现方式,发现即使小小的一枚注册中心实现,也蕴藏有丰富亮点供以学习、CV。
一般来说ConversionService 天生具备非常强悍的转换能力,因此实际情况是你若需要自定义一个Printer/Parser的话是大概率不需要自己再额外加个Converter转换器的,也就是说底层机制让你已然站在了“巨人”肩膀上。
♨本文思考题♨
看完了不一定懂,看懂了不一定会。来,文末3个思考题帮你复盘:
- FormatterRegistry作为注册中心只有添加方法,why?
- 示例中为何强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,会有什么问题?
- 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?