BeanUtilsツール類を捨てろ、MapStructはいい香りだ!!!

10098 ワード

先日の記事「なぜアリババはApache Beanutilsを使用して属性のcopyを禁止したのですか?」では、いくつかの属性コピーのツールクラスを比較しました.
そしてコメントエリアでは、MapStructこそ本当の香りだとフィードバックした読者もいたので、時間を割いてMapStructを理解しました.その結果、これは本当に神仙のフレームワークで、鶏の香りを炒めていることに気づきました.
この記事では、MapStructの使い方を簡単に紹介し、他のいくつかのツールクラスと比較してみましょう.
なぜMapStructが必要なのですか?
まず、MapStructのようなフレームワークがどのようなシーンに適しているのか、なぜ市販されているのかについてお話しします.
ソフトウェアアーキテクチャ設計では、階層構造が最も一般的であり、最も重要な構造でもあります.多くの人は3層アーキテクチャ、4層アーキテクチャなどに慣れていない.
「コンピューター科学分野のいかなる問題も間接的な中間層を増やすことで解決でき、だめなら2層を加える」という人もいる.
しかし、ソフトウェアアーキテクチャの階層化が進むにつれて、各階層間のデータモデルは相互変換の問題に直面し、典型的には、DO、DTO、VOなどのコードに様々なOを見ることができる.
一般的に、同じデータモデルでは、異なる階層で異なるデータモデルを使用します.データストレージ層では、DOを使用してビジネスエンティティを抽象化します.ビジネスロジック層では、DTOを使用してデータ転送オブジェクトを表します.展示層に着くと,オブジェクトをVOにカプセル化してフロントエンドと対話する.
では、データの先端からデータ永続化層(先端まで透過)にデータの先端から透過するには、オブジェクト間の相互変換、すなわち異なるオブジェクトモデル間でマッピングを行う必要がある.
通常、get/setなどの方法でフィールドマッピング操作を1つずつ行うことができます.
personDTO.setName(personDO.getName());
personDTO.setAge(personDO.getAge());
personDTO.setSex(personDO.getSex());
personDTO.setBirthday(personDO.getBirthday());


しかし,このようなマッピングコードの作成は冗長でエラーが発生しやすいタスクである.MapStructなどの類似のフレームワークの目標は,自動化によりこの作業をできるだけ多く簡略化することである.
MapStructの使用
MapStruct(https://mapstruct.org/)はコードジェネレータであり、「コンフィギュレーションよりも約束が優れている」方法に基づくJava beanタイプ間のマッピングの実装を極めて簡略化している.生成されたマッピングコードは、純粋なメソッド呼び出しを使用するため、迅速で、タイプが安全で、理解しやすい.
コンフィギュレーションよりも優れたコンフィギュレーションは、コンフィギュレーションよりも優れており、コンフィギュレーションと呼ばれ、ソフトウェア開発者が決定する数を減らし、簡単なメリットを獲得し、活性を失わないようにするソフトウェア設計のモデルです.
PersonDOとPersonDTOの2つのクラスが相互変換される必要があると仮定します.クラス定義は次のとおりです.
public class PersonDO {
    private Integer id;
    private String name;
    private int age;
    private Date birthday;
    private String gender;
}

public class PersonDTO {
    private String userName;
    private Integer age;
    private Date birthday;
    private Gender gender;
}


MapStructを用いたbeanマッピングの方法を実証した.
MapStructを使用するには、まず関連するjarパッケージに依存する必要があります.maven依存方式を使用すると、次のようになります.
...

    1.3.1.Final

...

    
        org.mapstruct
        mapstruct
        ${org.mapstruct.version}
    

...

    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.8.1
            
                1.8 
                1.8 
                
                    
                        org.mapstruct
                        mapstruct-processor
                        ${org.mapstruct.version}
                    
                    
                
            
        
    



MapStructはコンパイラで変換コードを生成する必要があるため、maven-compiler-pluginプラグインでmapstruct-processorへの参照を構成する必要があります.この部分は後述する.
その後、マッピングを行うインタフェースを定義する必要があります.主なコードは次のとおりです.
@Mapper
interface PersonConverter {
    PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings(@Mapping(source = "name", target = "userName"))
    PersonDTO do2dto(PersonDO person);
}


注記@Mapperを使用して、do 2 dtoメソッドを定義するConverterインタフェースを定義します.メソッドのパラメータタイプはPersonDOで、パラメータタイプはPersonDTOです.このメソッドはPersonDOをPersonDTOに変換するために使用されます.
テストコードは次のとおりです.
public static void main(String[] args) {
    PersonDO personDO = new PersonDO();
    personDO.setName("Hollis");
    personDO.setAge(26);
    personDO.setBirthday(new Date());
    personDO.setId(1);
    personDO.setGender(Gender.MALE.name());
    PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
    System.out.println(personDTO);
}


出力結果:
PersonDTO{userName='Hollis', age=26, birthday=Sat Aug 08 19:00:44 CST 2020, gender=MALE}


私たちはMapStructを使ってPersonDOをPersonDTOに完璧に変えたことがわかります.
上のコードから分かるように,MapStructの使い方は比較的簡単で,主に@Mapper注釈に依存している.
しかし、多くの場合、互いに変換する必要がある2つのクラス間の属性名、タイプなどが完全に一致していない場合や、直接マッピングしたくない場合もあることを知っています.では、どのように処理すればいいのでしょうか.
実はMapStructもこの方面でよくやっています.
MapStruct処理フィールドマッピング
まず、変換する2つのクラスのソースオブジェクト属性がターゲットオブジェクト属性のタイプと名前と一致すると、対応する属性が自動的にマッピングされることを明確に示すことができます.
では、もし特別な状況に遭遇したらどう処理しますか?
名前が一致しないマッピング方法
上記の例では、PersonDOではユーザ名をnameで表し、PersonDTOではuserNameでユーザ名を表していますが、パラメータマッピングはどのように行われますか.
この場合、@Mapping注記を使用します.メソッド署名で、この注記を使用して、変換するソースオブジェクトの名前とターゲットオブジェクトの名前を指定するだけでいいです.nameの値をuserNameにマッピングするには、次のようにします.
@Mapping(source = "name", target = "userName")


自動マッピング可能なタイプ
名前の不一致に加えて、上記の例のようにPersonDOではStringタイプでユーザの性別を表し、PersonDTOではGenterの列挙でユーザの性別を表す特殊な場合がある.
このときタイプが一致しない場合は,相互変換の問題にかかわる必要がある.
実際には、MapStructは一部のタイプを自動的にマッピングし、例のようにStringタイプを自動的に列挙タイプに変換する必要はありません.
一般的には、次の場合に自動タイプ変換を行います.
  • 基本タイプおよびそれらに対応する包装タイプ.
  • 基本タイプのパッケージタイプとStringタイプの間の
  • Stringタイプと列挙タイプの間の
  • カスタム定数
    マッピングの変換中に一定の値を定義する場合はconstantを使用します.
    @Mapping(source = "name", constant = "hollis")
    
    

    タイプが一致しないマッピング方法
    また、上記の例では、Personというオブジェクトに住所という属性を追加する必要がある場合、PersonoDTOではHomeAddressクラスを個別に定義して住所を表すのが一般的ですが、PersonクラスではStringタイプを使用して住所を表すのが一般的です.
    これはHomeAddressとStringの間でJSONを用いて相互変換する必要があり,この場合もMapStructはサポートできる.
    public class PersonDO {
        private String name;
        private String address;
    }
    
    public class PersonDTO {
        private String userName;
        private HomeAddress address;
    }
    @Mapper
    interface PersonConverter {
        PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
    
        @Mapping(source = "userName", target = "name")
        @Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
        PersonDO dto2do(PersonDTO dto2do);
    
        default String homeAddressToString(HomeAddress address){
            return JSON.toJSONString(address);
        }
    }
    
    

    PersonConverterでメソッドを定義するだけです(PersonConverterはインタフェースなのでJDK 1.8以降のバージョンでdefaultメソッドを定義できます).このメソッドの役割はHomeAddressをStringタイプに変換することです.
    defaultメソッド:Java 8が導入した新しい言語特性は、キーワードdefaultで表記され、defaultで表記されるメソッドは、実装を提供する必要がありますが、サブクラスは実装を選択するか、実装しないかを選択できます.
    次に、dto 2 doメソッドでは、次の注釈によってタイプの変換を実現することができる.
    @Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
    
    

    これはカスタムタイプ変換であり、StringとDateの間の変換など、MapStruct自体がサポートしているタイプ変換もあります.
    @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
    
    

    以上、よく使われるフィールドマッピングの方法を簡単に紹介しましたが、私自身が仕事でよく遭遇するいくつかのシーンでもあります.より多くの場合、公式の例を見ることができます(https://github.com/mapstruct/...).
    MapStructのパフォーマンス
    前述したように多くのMapStructの使い方は、MapStructの使い方が簡単で、フィールドマッピングの機能が強いことがわかりますが、彼の性能はどうでしょうか.
    なぜアリババはApache Beanutilsを使用して属性のcopyを禁止したのですか?」の例を参照して、MapStructのパフォーマンステストを行います.
    1000、10000、100000、100000のマッピングをそれぞれ実行する時間は、0 ms、1 ms、3 ms、6 msである.
    MapStructの消費時間は他のいくつかのツールに比べて非常に短いことがわかります.
    では、なぜMapStructの性能がこんなに良いのでしょうか.
    実際、MapStructと他のいくつかのフレームワークの最大の違いは、他のマッピングフレームワークと比較して、MapStructがコンパイル時にbeanマッピングを生成することであり、高性能を確保し、問題を事前にフィードバックすることができ、開発者が徹底的にエラーチェックを行うことができることです.
    前にMapStructの依存を導入したとき、特にmaven-compiler-pluginにmapstruct-processorのサポートを追加したことを覚えていますか?
    さらに,我々はコードに多くのMapStructが提供する注釈を用い,コンパイル期間中にMapStructがbeanマッピングのコードを直接生成することができ,我々の代わりに多くのsetterとgetterを書いたことに相当する.
    コードに次のMapperを定義します.
    @Mapper
    interface PersonConverter {
        PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
    
        @Mapping(source = "userName", target = "name")
        @Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
        @Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
        PersonDO dto2do(PersonDTO dto2do);
    
        default String homeAddressToString(HomeAddress address){
            return JSON.toJSONString(address);
        }
    }
    
    

    コードコンパイル後、PersonConverterImplが自動的に生成されます.
    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2020-08-09T12:58:41+0800",
        comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
    )
    class PersonConverterImpl implements PersonConverter {
    
        @Override
        public PersonDO dto2do(PersonDTO dto2do) {
            if ( dto2do == null ) {
                return null;
            }
    
            PersonDO personDO = new PersonDO();
    
            personDO.setName( dto2do.getUserName() );
            if ( dto2do.getAge() != null ) {
                personDO.setAge( dto2do.getAge() );
            }
            if ( dto2do.getGender() != null ) {
                personDO.setGender( dto2do.getGender().name() );
            }
    
            personDO.setAddress( homeAddressToString(dto2do.getAddress()) );
    
            return personDO;
        }
    }
    
    

    実行期間中にbeanをマッピングすると、PersonConverterImplのdto 2 doメソッドが直接呼び出され、特別なことは何もせず、メモリの中でsetやgetを行うだけでいいです.
    したがって,コンパイル期間中に多くのことをしたため,MapStructは実行期間中の性能が良好であり,問題の暴露をコンパイル期間に繰り上げることができるという利点もある.
    コード内のフィールドマッピングに問題がある場合、アプリケーションはコンパイルできず、開発者にこの問題を解決するように強制します.
    まとめ
    Javaのフィールドマッピングツールクラス、MapStructを紹介しました.彼の使い方は簡単で、機能が非常に完備していて、いろいろな状況のフィールドマッピングに対応することができます.
    また、コンパイル期間であるため、本格的なマッピングコードが生成され、実行期間のパフォーマンスが大幅に向上します.
    強くお勧めして、本当に香ばしい!!!