9. 細節見真章,Formatter註冊中心的設計很討巧

✍前言

你好,我是A哥(YourBatman)。

Spring設計了org.springframework.format.Formatter格式化器接口抽象,對格式化器進行了大一統,讓你只需要關心統一的API,而無需關注具體實現,相關議題上篇文章 有詳細介紹。

Spring內建有不少格式化器實現,同時對它們的管理、調度使用也有專門的組件負責,可謂涇渭分明,職責清晰。本文將圍繞Formatter註冊中心FormatterRegistry展開,爲你介紹Spring是如何優雅,巧妙的實現註冊管理的。

學習編碼是個模仿的過程,絕大多數時候你並不需要創造東西。當然這裏指的模仿並非普通的CV模式,而是取精華爲己所用,本文所述巧妙設計便是精華所在,任君提取。

這幾天進入小寒天氣,北京迎來最低-20℃,最高-11℃的冰點溫度,外出注意保暖

本文提綱

版本約定

  • Spring Framework:5.3.x
  • Spring Boot:2.4.x

✍正文

對Spring的源碼閱讀、分析這麼多了,會發現對於組件管理大體思想都一樣,離不開這幾個組件:註冊中心(註冊員) + 分發器

一龍生九子,九子各不同。雖然大體思路保持一致,但每個實現在其場景下都有自己的發揮空間,值得我們向而往之。

FormatterRegistry:格式化器註冊中心

field屬性格式化器的註冊表(註冊中心)。請注意:這裏強調了field的存在,先混個眼熟,後面你將能有較深體會。

public interface FormatterRegistry extends ConverterRegistry {

	void addPrinter(Printer<?> printer);
	void addParser(Parser<?> parser);
	void addFormatter(Formatter<?> formatter);
	void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
	void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

	void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

此接口繼承自類型轉換器註冊中心ConverterRegistry,所以格式化註冊中心是轉換器註冊中心的加強版,是其超集,功能更多更強大。

關於類型轉換器註冊中心ConverterRegistry的詳細介紹,可翻閱本系列的這篇文章,看完後門清

雖然FormatterRegistry提供的添加方法挺多,但其實基本都是在描述同一個事:爲指定類型fieldType添加格式化器(printer或parser),繪製成圖如下所示:

說明:最後一個接口方法除外,addFormatterForFieldAnnotation()和格式化註解相關,因爲它非常重要,因此放在下文專門撰文講解

FormatterRegistry接口的繼承樹如下:

有了學過ConverterRegistry的經驗,這種設計套路很容易被看穿。這兩個實現類按層級進行分工:

  • FormattingConversionService:實現所有接口方法
  • DefaultFormattingConversionService:繼承自上面的FormattingConversionService,在其基礎上註冊默認的格式化器

事實上,功能分類確實如此。本文重點介紹FormattingConversionService,這個類的設計實現上有很多討巧之處,只要你來,要你好看。

FormattingConversionService

它是FormatterRegistry接口的實現類,實現其所有接口方法。

FormatterRegistryConverterRegistry的子接口,而ConverterRegistry接口的所有方法均已由GenericConversionService全部實現了,所以可以通過繼承它來間接完成 ConverterRegistry接口方法的實現,因此本類的繼承結構是這樣子的(請細品這個結構):

FormattingConversionService通過繼承GenericConversionService搞定“左半邊”(父接口ConverterRegistry);只剩“右半邊”待處理,也就是FormatterRegistry新增的接口方法。

FormattingConversionService:

	@Override
	public void addPrinter(Printer<?> printer) {
		Class<?> fieldType = getFieldType(printer, Printer.class);
		addConverter(new PrinterConverter(fieldType, printer, this));
	}
	@Override
	public void addParser(Parser<?> parser) {
		Class<?> fieldType = getFieldType(parser, Parser.class);
		addConverter(new ParserConverter(fieldType, parser, this));
	}
	@Override
	public void addFormatter(Formatter<?> formatter) {
		addFormatterForFieldType(getFieldType(formatter), formatter);
	}
	@Override
	public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
		addConverter(new PrinterConverter(fieldType, formatter, this));
		addConverter(new ParserConverter(fieldType, formatter, this));
	}
	@Override
	public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
		addConverter(new PrinterConverter(fieldType, printer, this));
		addConverter(new ParserConverter(fieldType, parser, this));
	}

從接口的實現可以看到這個“驚天大祕密”:所有的格式化器(含Printer、Parser、Formatter)都是被當作Converter註冊的,也就是說真正的註冊中心只有一個,那就是ConverterRegistry

格式化器的註冊管理遠沒有轉換器那麼複雜,因爲它是基於上層適配的思想,最終適配爲Converter來完成註冊的。所以最終註冊進去的實際是個經由格式化器適配來的轉換器,完美複用了那套複雜的轉換器管理邏輯。

這種設計思路,完全可以“CV”到我們自己的編程思維裏吧

甭管是Printer還是Parser,都會被適配爲GenericConverter從而被添加到ConverterRegistry裏面去,被當作轉換器管理起來。現在你應該知道爲何FormatterRegistry接口僅需提供添加方法而無需提供刪除方法了吧。

當然嘍,關於Printer/Parser的適配實現亦是本文本文關注的焦點,裏面大有文章可爲,let's go!

PrinterConverter:Printer接口適配器

Printer<?>適配爲轉換器,轉換目標爲fieldType -> String

private static class PrinterConverter implements GenericConverter {
	
	private final Class<?> fieldType;
	// 從Printer<?>泛型裏解析出來的類型,有可能和fieldType一樣,有可能不一樣
	private final TypeDescriptor printerObjectType;
	// 實際執行“轉換”動作的組件
	private final Printer printer;
	private final ConversionService conversionService;

	public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
		...
		// 從類上解析出泛型類型,但不一定是實際類型
		this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
		...
	}

	// fieldType -> String
	@Override
	public Set<ConvertiblePair> getConvertibleTypes() {
		return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
	}

}

既然是轉換器,重點當然是它的convert轉換方法:

PrinterConverter:

	@Override
	@SuppressWarnings("unchecked")
	public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		// 若sourceType不是printerObjectType的子類型
		// 就嘗試用conversionService轉一下類型試試
		// (也就是說:若是子類型是可直接處理的,無需轉換一趟)
		if (!sourceType.isAssignableTo(this.printerObjectType)) {
			source = this.conversionService.convert(source, sourceType, this.printerObjectType);
		}
		if (source == null) {
			return "";
		}

		// 執行實際轉換邏輯
		return this.printer.print(source, LocaleContextHolder.getLocale());
	}

轉換步驟分爲兩步:

  1. 類型(實際類型)不是該Printer類型的泛型類型的子類型的話,那就嘗試使用conversionService轉一趟
    1. 例如:Printer處理的是Number類型,但是你傳入的是Person類型,這個時候conversionService就會發揮作用了
  2. 交由目標格式化器Printer執行實際的轉換邏輯

可以說Printer它可以直接轉,也可以是構建在conversionService 之上 的一個轉換器:只要源類型是我處理的,或者經過conversionService後能成爲我處理的類型,都能進行轉換。有一次完美的能力複用

說到這我估計有些小夥伴還不能理解啥意思,能解決什麼問題,那麼下面我分別給你用代碼舉例,加深你的瞭解。

準備一個Java Bean:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {

    private Integer id;
    private String name;
}

準備一個Printer:將Integer類型加10後,再轉爲String類型

private static class IntegerPrinter implements Printer<Integer> {

    @Override
    public String print(Integer object, Locale locale) {
        object += 10;
        return object.toString();
    }
}

示例一:使用Printer,無中間轉換

測試用例:

@Test
public void test2() {
    FormattingConversionService formattingConversionService = new FormattingConversionService();
    FormatterRegistry formatterRegistry = formattingConversionService;
    // 說明:這裏不使用DefaultConversionService是爲了避免默認註冊的那些轉換器對結果的“干擾”,不方便看效果
    // ConversionService conversionService = new DefaultConversionService();
    ConversionService conversionService = formattingConversionService;

    // 註冊格式化器
    formatterRegistry.addPrinter(new IntegerPrinter());

    // 最終均使用ConversionService統一提供服務轉換
    System.out.println(conversionService.canConvert(Integer.class, String.class));
    System.out.println(conversionService.canConvert(Person.class, String.class));

    System.out.println(conversionService.convert(1, String.class));
    // 報錯:No converter found capable of converting from type [cn.yourbatman.bean.Person] to type [java.lang.String]
    // System.out.println(conversionService.convert(new Person(1, "YourBatman"), String.class));
}

運行程序,輸出:

true
false
11

完美。

但是,它不能完成Person -> String類型的轉換。一般來說,我們有兩種途徑來達到此目的:

  1. 直接方式:寫一個Person轉String的轉換器,專用
    1. 缺點明顯:多寫一套代碼
  2. 組合方式(推薦):如果目前已經有Person -> Integer的了,那我們就組合起來用就非常方便啦,下面這個例子將告訴你使用這種方式完成“需求”
    1. 缺點不明顯:轉換器一般要求與業務數據無關,因此通用性強,應最大可能的複用

下面示例二將幫你解決通過複用已有能力方式達到Person -> String的目的。

示例二:使用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轉換一把

可以看到它和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)

完美。

針對本例,有如下關注點:

  1. 使用addFormatterForFieldType()方法註冊了IntegerParser,並且明確指定了處理的類型,用於處理Person類型
    1. 也就是說此IntegerParser專門用於轉換目標類型爲Person的屬性
  2. 因爲IntegerParser 實際上 只能轉換 String -> Integer,因此還必須註冊一個轉換器,用於Integer -> Person橋接一下,這樣就串起來了String -> Integer -> Person。外面看起來這些都是IntegerParser做的一樣,非常工整
  3. 同樣強調: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引用,從而到這樣的效果:

ConversionService

通過能力組合協作,起到串聯作用,從而擴大輸入/輸出“範圍”,感覺就像起到了放大鏡的效果一樣,這個設計還是很討巧的。

✍總結

本文以介紹FormatterRegistry接口爲中心,重點研究了此接口的實現方式,發現即使小小的一枚註冊中心實現,也蘊藏有豐富亮點供以學習、CV。

一般來說ConversionService 天生具備非常強悍的轉換能力,因此實際情況是你若需要自定義一個Printer/Parser的話是大概率不需要自己再額外加個Converter轉換器的,也就是說底層機制讓你已然站在了“巨人”肩膀上。

♨本文思考題♨

看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你覆盤:

  1. FormatterRegistry作爲註冊中心只有添加方法,why?
  2. 示例中爲何強調:addConverter()註冊轉換器時請務必不要使用lambda表達式代替輸入,會有什麼問題?
  3. 這種功能組合/橋接的巧妙設計方式,你腦中還能想到其它案例嗎?

☀推薦閱讀☀

♚聲明♚

本文所屬專欄:Java進階,公號後臺回覆專欄名即可獲取全部內容。

分享、成長,拒絕淺藏輒止。關注【BAT的烏托邦】,回覆關鍵字專欄有Spring技術棧、中間件等小而美的原創專欄供以免費學習。本文已被 https://www.yourbatman.cn 收錄。

本文是 A哥(YourBatman)原創文章,未經作者允許/開白不得轉載,謝謝合作。

BAT的烏托邦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章