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. 这种功能组合/桥接的巧妙设计方式,你脑中还能想到其它案例吗?
相关文章
|
5月前
|
存储 缓存 测试技术
微服务注册中心的原理和实现方式
【2月更文挑战第19天】注册中心可以说是实现服务化的关键,因为服务化之后,服务提供者和服务消费者不在同一个进程中运行,实现了解耦,这就需要一个纽带去连接服务提供者和服务消费者,而注册中心就正好承担了这一角色。
|
12月前
|
存储 运维 Dubbo
Nacos 注册中心的设计原理:让你的应用轻松实现高效注册与发现!
Nacos 注册中心的设计原理:让你的应用轻松实现高效注册与发现!
160 0
|
2月前
|
开发工具 Android开发
Android项目架构设计问题之外部客户方便地设置回调如何解决
Android项目架构设计问题之外部客户方便地设置回调如何解决
19 0
|
5月前
|
移动开发 前端开发
基于若依的ruoyi-nbcio流程管理系统自定义业务实现一种简单的动态任务标题(续)
基于若依的ruoyi-nbcio流程管理系统自定义业务实现一种简单的动态任务标题(续)
43 1
|
5月前
|
移动开发 前端开发
基于若依的ruoyi-nbcio流程管理系统自定义业务实现一种简单的动态任务标题需求
基于若依的ruoyi-nbcio流程管理系统自定义业务实现一种简单的动态任务标题需求
72 1
|
5月前
|
小程序 数据格式
【经验分享】如何实现自定义数据源的级联选择组件?
【经验分享】如何实现自定义数据源的级联选择组件?
76 6
|
存储 缓存 监控
聊聊消息中心的设计与实现逻辑
消息通知的流程设计,在各个业务线中通过消息中心提供的接口方法,将不同场景下的消息内容提交到消息中心,消息中心进行统一维护管理,并根据消息的来源和去向,适配相应的推送逻辑。
1148 0
聊聊消息中心的设计与实现逻辑
|
Kubernetes 监控 Cloud Native
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
Go微服务架构实战 中篇:2. 基于k8s部署服务和注册中心,验证服务注册和发现
|
Java 数据库 微服务
微服务项目:尚融宝(36)(核心业务流程:用户绑定(1))
尚融宝应该在网站引导客户在“资金托管平台“开设账户。也就是说投资人和借款人必须在资金托管平台页面上自主开户,与尚融宝建立对应的账户体系。
微服务项目:尚融宝(36)(核心业务流程:用户绑定(1))
|
前端开发 数据库 微服务
微服务项目:尚融宝(37)(核心业务流程:用户绑定(2))
微服务项目:尚融宝(37)(核心业务流程:用户绑定(2))
微服务项目:尚融宝(37)(核心业务流程:用户绑定(2))