9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

こんにちは、私の名前はブラザーA(YourBatman)です。

Springは、org.springframework.format.Formatterフォーマッターインターフェイスの抽象化を設計し、フォーマッターを統合して、特定の実装ではなく統合APIのみを気にする必要があるようにしました。関連トピックについては、前の記事で詳しく説明しています

Springには多くのフォーマッター実装が組み込まれており、それらの管理、スケジューリング、および使用を担当する特別なコンポーネントがあります。これは、明確で明確な責任として説明できます。この記事では、Formatter登録センターに焦点を当て、FormatterRegistrySpringが登録管理をエレガントかつ巧妙に実装する方法を紹介します。

コーディングを学ぶことは模倣のプロセスであり、ほとんどの場合、何かを作成する必要はありません。もちろん、ここでいう模造品は普通のCVモデルはなく、エッセンスを自分で使うためのものです。この記事で説明する独創的なデザインがエッセンスであり、それを抽出することができます。

ここ数日、少し寒い気候になりました。北京の氷点下気温は-20℃、-11℃です。外出時は保温してください。

この記事の概要

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

バージョン規則

  • Spring Framework:5.3.x
  • スプリングブーツ:2.4.x

✍テキスト

Springのソースコードをたくさん読んで分析した後、コンポーネント管理の一般的な考え方は同じであり、これらのコンポーネントは分離できないことがわかります:レジストリ(レジストラ)+ディストリビュータ

1匹のドラゴンが9人の子供を産みますが、それぞれが異なります。一般的な考え方は同じですが、それぞれの実現にはシナリオ内に独自のスペースがあり、私たちの憧れに値します。

FormatterRegistry:フォーマッターレジストリ

フィールド属性フォーマッタのレジストリ(レジストリセンター)。注意:ここでは、フィールドの存在が強調されています。最初にフィールドに精通し、後でより深く理解するようになります。

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フォーマッタ(プリンタやパーサ)を追加指定された型に、以下のように図です。

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

注:最後のインターフェース方法を除いて、addFormatterForFieldAnnotation()それは非常に重要であるため、注釈のフォーマットに関連しているので、以下でそれを説明する記事を書きます

FormatterRegistryインターフェイスの継承ツリーは次のとおりです。

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

ConverterRegistryの学習経験があるため、この設計ルーチンは簡単に確認できます。これらの2つの実装クラスは、階層的に分割されています。

  • FormattingConversionService:すべてのインターフェイスメソッドを実装する
  • DefaultFormattingConversionService:上記のFormattingConversionServiceから継承され、それに基づいてデフォルトのフォーマッタを登録します

実際、機能分類は確かに当てはまります。この記事ではFormattingConversionService、このクラスの設計と実装に焦点を当てています。多くのトリックがあります。あなたが来ている限り、見栄えを良くしてほしいと思っています。

FormattingConversionService

これはFormatterRegistry、インターフェイスの実装クラスであり、そのすべてのインターフェイスメソッドを実装します。

FormatterRegistryはいConverterRegistry、ConverterRegistryインターフェイスのすべてのメソッドがGenericConversionService完全に実装されているため ConverterRegistryインターフェイスメソッドの実装を継承することで間接的に完了することができます。したがって、このクラスの継承構造は次のようになります(この構造を歌ってください)。

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

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));
    }

インターフェイスの実装から、この「衝撃的な秘密」を確認できます。すべてのフォーマッタ(プリンタ、パーサー、フォーマッタを含む)はConverter登録済みと見なされますConverterRegistryつまり、実際のレジストリは1つだけです

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

フォーマッタの登録管理は、上位レベルの適応のアイデアに基づいており、最終的にコンバータに適応して登録を完了するため、コンバータの登録管理よりもはるかに複雑でありませんしたがって、実際の最終登録は、フォーマッターを介してコンバーターに適合され、コンバーター管理ロジックの複雑なセットを完全に再利用します。

この設計アイデアは、私たち自身のプログラミング思考に「CV」することができます。

それがプリンターであるかパーサーであるかに関係なく、両方がGenericConverterに適合され、ConverterRegistryそれに追加され、コンバーターとして管理されます。これで、FormatterRegistryインターフェイスが追加メソッドのみを提供する必要があり、削除メソッドは提供する必要がない理由を理解する必要があります。

もちろん、プリンター/パーサーの適応の実装もこの記事の焦点です。そこにはたくさんの記事があります。行きましょう!

PrinterConverter:プリンターインターフェースアダプター

Printer&lt;?&gt;適応変換器であり、変換対象ですfieldType -&gt; 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));
    }

}

これはコンバーターであるため、重要なポイントはもちろんその変換変換方法です。

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());
    }

変換ステップは2つのステップに分かれています。

  1. ソースタイプ(実際のタイプ)がプリンタータイプのジェネリックタイプのサブタイプでない場合は、conversionServiceを使用して転送してみてください
    1. 例:PrinterはNumberタイプを処理しますが、Personタイプを渡すと、今回はconversionServiceが機能します
  2. ターゲットフォーマッタプリンタに実際の変換ロジックを実行させます

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

プリンターは、直接転送できる、コンバーター上にconversionService構築できる、と言うことができます。ソースが処理できるタイプである限り、またはconvertingServiceが私のエネルギータイプの処理になった後、変換できます完璧な能力の再利用があります。

そういえば、それが何を意味し、どのような問題を解決できるのか、まだ理解できない友達もいると思いますので、理解を深めるためのコード例を紹介します。

JavaBeanを準備します。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {

    private Integer id;
    private String name;
}

プリンターの準備:整数型に10を追加してから、文字列型に変換します

private static class IntegerPrinter implements Printer<Integer> {

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

例1:プリンターを使用し、中間変換なし

テストケース:

@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 -&gt; String型変換は完了できません一般的に、この目標を達成するには2つの方法があります。

  1. 直接的な方法:専用のPerson toStringコンバーターを作成する
    1. 短所は明らかです:より多くのコードを書く
  2. 組み合わせ(推奨):すでに存在する場合Person -&gt; Integer、それを非常に便利なものと組み合わせると、次の例では、この方法を使用して「デマンド」を完了することがわかります。
    1. 欠点は明らかではありません。コンバーターは通常、ビジネスデータとは何の関係も必要としないため、用途が広く、可能な限り再利用する必要があります。

次の例2は既存の機能を再利用するPerson -&gt; Stringことで達成される目的を解決するのに役立ちます

例2:中間変換でプリンターを使用する

例1に基づいPerson -&gt; Stringて、それを実現たい場合は、別のPerson -&gt; 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());整数を処理するためにそれ維持することができます->文字列は問題です
  2. IntegerPrinter実際には変換しかできないためInteger -&gt; StringPerson -&gt; Integer接続できるように、ブリッジング用のコンバーターを登録する必要ありますPerson -&gt; Integer -&gt; Stringこれらは外部からIntegerPrinterによって作成されているように見えますこれは非常にきれいです。
  3. 強調:コンバーターをaddConverter()に登録するときは、入力の代わりにラムダ式を使用しないでください。使用しないと、ジェネリック型が失われ、エラーが発生します。
    1. ラムダ式を使用する場合は、オーバーロードメソッドaddConverter(Class、Class、Converter)を使用して登録を完了してください

ParserConverter:パーサーインターフェイスアダプター

Parser&lt;?&gt;適応変換器であり、変換対象ですString -&gt; 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));
    }

}

これはコンバーターであるため、重要なポイントはもちろんその変換変換方法です。

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;
    }

変換ステップは2つのステップに分かれています。

  1. パーサーと結果を使用して文字列を指定されたタイプに変換します(失敗した場合、例外がスローされます)
  2. 結果がターゲットタイプのサブタイプであるかどうかを判断し、直接返すか、ConversionServiceを呼び出して変換します

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

プリンターの「順番」とは逆で、戻り値に大騒ぎしていることがわかります。同様に、以下の2つの例を使用して理解を深めます。

private static class IntegerParser implements Parser<Integer> {

    @Override
    public Integer parse(String text, Locale locale) throws ParseException {
        return NumberUtils.parseNumber(text, Integer.class);
    }
}

例1:パーサーを使用し、中間変換なし

テストケースを書く:

@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

完璧です。

例2:中間変換でパーサーを使用する

次の例では、「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()にすることができます。メソッドの2番目のパラメーターはnullを渡すことができません。それ以外の場合は、nullポインターを渡すことができます。これは実際にSpring Frameworkはバグです。コミュニティに問題を提出しましたが、解決できることを願っています。

9.詳細については、実際の章を参照してください。Formatterレジストリのデザインはとてもかわいいです。

この例を正しく実行するには、次のように変更します。

// 第二个参数不传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 -&gt; IntegerInteger -&gt; Personブリッジ用のコンバーターを登録して、ストリング化できるようする必要がありますString -&gt; Integer -&gt; Personそれはのように見えるこれらはIntegerParserによって作られ、非常にきれい
  3. また、コンバーターをaddConverter()に登録するときは、入力の代わりにラムダ式を使用しないように注意してください。使用しないと、ジェネリック型が失われ、エラーが発生します。

ConversionServiceを保持することによってどのような機能強化がもたらされますか?

注:このような重要なConversionServiceについて知っています。忘れた場合は、エレベーターでこのレビューにアクセスできます。

PrinterConverterおよびParserConverterの場合、それらのソースの目的はを達成することString &lt;-&gt; Objectであり、特性は次のとおりです。

  • PrinterConverter:出口はString型である必要があり、入口型も決定されています。つまりPrinter&lt;T&gt;、処理のみが可能な汎用型です。T(或T的子类型) -&gt; String
  • ParserConverter:エントリはString型である必要があり、出口型も決定されています。つまりParser&lt;T&gt;、処理のみが可能な汎用型です。String -&gt; T(或T的子类型)

確立された「ルール」によると、それらの能力の範囲はまだかなり制限されています。これは、Springが非常に強力であるため、組み合わせることで既存のコンポーネントの機能を巧みに拡張できる場所です。たとえば、次の効果を実現するために、PrinterConverter / ParserConverterのConversionService参照を利益に入れます。

ConversionService

機能とコラボレーションの組み合わせにより、直列の役割を果たし、拡大鏡のような入出力の「範囲」を広げます。このデザインは今でもとてもかわいいです。

✍まとめ

この記事でFormatterRegistryインターフェイスの導入に焦点を当て、このインターフェイスの実装に焦点を当てています。小さなレジストリセンターでも、学習とCVに関する豊富なハイライトがあることがわかりました。

一般的に、非常に強力な変換機能を備えてConversionService 生まれているため、実際の状況では、プリンター/パーサーをカスタマイズする必要がある場合、コンバーターコンバーターを自分で追加する必要がない可能性が高くなります。つまり、基盤となるメカニズムにより、すでに立ち上がっています。 「巨人」の肩に。

♨この記事は質問を考えています♨

読んだ後は理解できないかもしれませんし、理解できないかもしれません。ここでは、記事の最後にある3つの考えられる質問がレビューに役立ちます。

  1. 登録センターとしてのFormatterRegistryには、追加する方法しかありません。なぜですか。
  2. 例で強調されているのはなぜですか:addConverter()でコンバーターを登録するときは、入力の代わりにラムダ式を使用しないようにしてください。問題は何ですか?
  3. 機能の組み合わせ/ブリッジングのこの巧妙な設計の他のケースを考えることができますか?

☀推奨読書☀

♚ステートメント♚

この記事は次の列に属しています:Springタイプの変換、すべてのコンテンツを取得するための列名へのパブリックアカウントの舞台裏の返信。

共有し、成長し、隠れることを拒否し、停止します。[BAT Utopia]に注目し、Springテクノロジースタックやミドルウェアなど、小さくて美しいオリジナルのコラムキーワードコラムに返信して、無料で学習しましょう。この記事はhttps://www.yourbatman.cnが含まれています。

この記事はA兄弟(YourBatman)のオリジナル記事であり、作者の許可・開封なしに複製することはできません。ご協力ありがとうございます。

おすすめ

転載: blog.51cto.com/3631118/2592729