9. 细节见真章,Formatter注册中心的设计很讨巧(下)

简介: 9. 细节见真章,Formatter注册中心的设计很讨巧(下)

示例二:使用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;
  }


转换步骤分为两步:


  1. 通过Parser将String转换为指定的类型结果result(若失败,则抛出异常)
  2. 判断若result属于目标类型的子类型,直接返回,否则调用ConversionService转换一把


image.png


可以看到它和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,期待能够被解决喽:


image.png



为了正常运行本例,这么改一下:


/

// 第二个参数不传null,用IntegerPrinter占位
formatterRegistry.addFormatterForFieldType(Person.class, new IntegerPrinter(), new IntegerParser()


再次运行程序,输出:

true
Person(id=1, name=YourBatman)


完美。


针对本例,有如下关注点:


  1. 使用addFormatterForFieldType()方法注册了IntegerParser,并且明确指定了处理的类型,用于处理Person类型
  2. 也就是说此IntegerParser专门用于转换目标类型为Person的属性
  3. 因为IntegerParser 实际上 只能转换 String -> Integer,因此还必须注册一个转换器,用于Integer -> Person桥接一下,这样就串起来了String -> Integer -> Person。外面看起来这些都是IntegerParser做的一样,非常工整
  4. 同样强调: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引用,从而到这样的效果:

image.png



通过能力组合协作,起到串联作用,从而扩大输入/输出“范围”,感觉就像起到了放大镜的效果一样,这个设计还是很讨巧的。


✍总结


本文以介绍FormatterRegistry接口为中心,重点研究了此接口的实现方式,发现即使小小的一枚注册中心实现,也蕴藏有丰富亮点供以学习、CV。


一般来说ConversionService 天生具备非常强悍的转换能力,因此实际情况是你若需要自定义一个Printer/Parser的话是大概率不需要自己再额外加个Converter转换器的,也就是说底层机制让你已然站在了“巨人”肩膀上。


♨本文思考题♨


看完了不一定懂,看懂了不一定会。来,文末3个思考题帮你复盘:


  1. FormatterRegistry作为注册中心只有添加方法,why?
  2. 示例中为何强调:addConverter()注册转换器时请务必不要使用lambda表达式代替输入,会有什么问题?
  3. 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?
相关文章
|
人工智能 自然语言处理 测试技术
在PyCharm中提升编程效率:通义灵码(DeepSeek)助手全攻略(新版)
最近小栈在PyCharm中使用了阿里的 通义灵码 插件还不错,本次就再分享一个好用的AI代码助手,让编码过程更加方便!
6440 23
|
量子技术
量子雷达:隐身技术的挑战者与未来防御系统
【9月更文挑战第19天】量子雷达凭借其突破隐身技术、高灵敏度及抗干扰性的优势,正成为未来防御系统的关键组成部分。本文深入探讨了量子雷达如何挑战传统隐身技术,并介绍了其在反隐身作战、导弹防御及空间探测等领域的广阔应用前景。随着技术进步,量子雷达将彻底改变现代战争模式,提升防御体系的效能。中国在这一领域已取得显著进展,展现出量子雷达的强大潜力。
FFmpeg学习笔记(二):多线程rtsp推流和ffplay拉流操作,并储存为多路avi格式的视频
这篇博客主要介绍了如何使用FFmpeg进行多线程RTSP推流和ffplay拉流操作,以及如何将视频流保存为多路AVI格式的视频文件。
1774 0
|
网络协议 Linux
云服务器内部端口占用,9090端口已经存在了,如何关闭,Linux查询端口,查看端口,端口查询,关闭端口写法-netstat -tuln,​fuser -k 3306/tcp​
云服务器内部端口占用,9090端口已经存在了,如何关闭,Linux查询端口,查看端口,端口查询,关闭端口写法-netstat -tuln,​fuser -k 3306/tcp​
|
3天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23294 2
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
5天前
|
人工智能 API 开发工具
Claude Code国内安装:2026最新保姆教程(附cc-switch配置)
Claude Code是我目前最推荐的AI编程工具,没有之一。 它可能不是最简单的,但绝对是上限最高的。一旦跑通安装、接上模型、定好规范,你会发现很多原本需要几小时的工作,现在几分钟就能搞定。 这套方案的核心优势就三个字:可控性。你不用依赖任何不稳定服务,所有组件都在自己手里。模型效果不好?换一个。框架更新了?自己决定升不升。 这才是AI时代开发者该有的姿势——不是被动等喂饭,而是主动搭建自己的生产力基础设施。 希望这篇保姆教程,能帮你顺利上车。做出你自己的作品。
7981 18
Claude Code国内安装:2026最新保姆教程(附cc-switch配置)
|
12天前
|
缓存 人工智能 自然语言处理
我对比了8个Claude API中转站,踩了不少坑,总结给你
本文是个人开发者耗时1周实测的8大Claude中转平台横向评测,聚焦Claude Code真实体验:以加权均价(¥/M token)、内部汇率、缓存支持、模型真实性及稳定性为核心指标。
4744 24
|
8天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
3328 11
|
7天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
2711 9
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病