プロジェクトロンボク入門

Project Lombokの概要

Lombokは、私が文字通り常にプロジェクトにドロップするツールの1つであり、最初にビルドします。 最近、JavaなしでJavaをプログラミングすることは想像できませんでした。 この記事を読んでその力を見つけてください。

1. 繰り返しのコードを避ける

Javaは優れた言語ですが、一般的なタスクや一部のフレームワークの慣行を順守するためにコードで実行する必要があるものに対して、冗長すぎる場合があります。 これらはあなたのプログラムのビジネス側に本当の価値をもたらさないことが非常に多いのです。そしてこれがあなたの人生をより幸せにし、あなた自身の生産性を上げるためにロンボクがここにいる場所です。

それが機能する方法は、ビルドプロセスにプラグインし、コードで導入するプロジェクトアノテーションの数に従って、Javaバイトコードを.classファイルに自動生成することです。

参考文献:

デフォルト値のロンボクビルダー

Lombokを使用してビルダーのデフォルトプロパティ値を作成する方法を学ぶ

EclipseとIntellijを使用してLombokをセットアップする

一般的なIDEでLombokをセットアップする方法を学ぶ

使用するシステムに関係なく、ビルドに含めることは非常に簡単です。 彼らのproject pageには、詳細についての詳細な説明があります。 私のプロジェクトのほとんどはMavenベースであるため、通常はそれらの依存関係をprovidedスコープにドロップするだけで、次のようになります。


    ...
    
        org.projectlombok
        lombok
        1.18.4
        provided
    
    ...

利用可能な最新バージョンhereを確認します。

Lombokに依存しても、.jarsのユーザーもそれに依存することはありません。これは、ランタイムではなく、純粋なビルドの依存関係であるためです。

2. Getters/Setters, Constructors – So Repetitive

パブリックゲッターメソッドとセッターメソッドを介してオブジェクトプロパティをカプセル化することは、Javaの世界では非常に一般的な慣行であり、多くのフレームワークはこの「Java Bean」パターンに広く依存しています。 「プロパティ」。

これは非常に一般的であるため、ほとんどのIDEは、これらのパターン(およびその他)の自動生成コードをサポートしています。 ただし、このコードはソースに存在する必要があり、たとえば新しいプロパティが追加されたり、フィールドの名前が変更されたときにも維持する必要があります。

例として、JPAエンティティとして使用するこのクラスについて考えてみましょう。

@Entity
public class User implements Serializable {

    private @Id Long id; // will be set when persisting

    private String firstName;
    private String lastName;
    private int age;

    public User() {
    }

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    // getters and setters: ~30 extra lines of code
}

これはかなり単純なクラスですが、ゲッターとセッターのコードを追加すると、関連するビジネス情報よりも定型的なゼロ値コードが多くなるという定義になってしまうことを考慮してください。姓と年齢。」

このクラスをLombok-izeにしましょう:

@Entity
@Getter @Setter @NoArgsConstructor // <--- THIS is it
public class User implements Serializable {

    private @Id Long id; // will be set when persisting

    private String firstName;
    private String lastName;
    private int age;

    public User(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

@Getterおよび@Setterアノテーションを追加することにより、クラスのすべてのフィールドに対してこれらを生成するようにLombokに指示しました。 @NoArgsConstructorは、空のコンストラクター生成につながります。

これはwholeクラスコードであることに注意してください。// getters and settersコメント付きの上記のバージョンとは対照的に、私は何も省略していません。 関連する3つの属性クラスの場合、これはコードの大幅な節約になります!

Userクラスに属性(プロパティ)をさらに追加すると、同じことが起こります。型自体にアノテーションを適用して、デフォルトですべてのフィールドを気にするようにしました。

一部のプロパティの可視性を改善したい場合はどうしますか? たとえば、エンティティのidフィールド修飾子packageまたはprotectedは、読み取られることが期待されていますが、アプリケーションコードによって明示的に設定されていないため、表示したままにしておきます。 この特定のフィールドには、より細かい@Setterを使用してください。

private @Id @Setter(AccessLevel.PROTECTED) Long id;

3. レイジーゲッター

多くの場合、アプリケーションは高価な操作を実行し、結果を保存して後で使用する必要があります。

たとえば、ファイルまたはデータベースから静的データを読み取る必要があるとします。 通常、このデータを1回取得してからキャッシュし、アプリケーション内でメモリ内の読み取りができるようにすることをお勧めします。 これにより、アプリケーションが高価な操作を繰り返すのを防ぎます。

もう1つの一般的なパターンは、retrieve this data only when it’s first neededです。 言い換えれば、only get the data when the corresponding getter is called the first time. This is called lazy-loading.

このデータがクラス内のフィールドとしてキャッシュされるとします。 クラスは、このフィールドへのアクセスがキャッシュされたデータを返すことを確認する必要があります。 このようなクラスを実装する1つの可能な方法は、フィールドがnullの場合にのみ、getterメソッドにデータを取得させることです。 このため、we call this a lazy getter

Lombokは、上記で見たlazy parameter in the @Getter annotationでこれを可能にします。

たとえば、次の単純なクラスを考えます。

public class GetterLazy {

    @Getter(lazy = true)
    private final Map transactions = readTxnsFromFile();

    private Map readTxnsFromFile() {

        final Map cache = new HashMap<>();
        List txnRows = readTxnListFromFile();

        txnRows.forEach(s -> {
            String[] txnIdValueTuple = s.split(DELIMETER);
            cache.put(txnIdValueTuple[0], Long.parseLong(txnIdValueTuple[1]));
        });

        return cache;
    }
}

これにより、一部のトランザクションがファイルからMapに読み込まれます。 ファイル内のデータは変更されないため、一度キャッシュして、ゲッター経由でのアクセスを許可します。

このクラスのコンパイル済みコードを見ると、getter method which updates the cache if it was null and then returns the cached dataが表示されます。

public class GetterLazy {

    private final AtomicReference transactions = new AtomicReference();

    public GetterLazy() {
    }

    //other methods

    public Map getTransactions() {
        Object value = this.transactions.get();
        if (value == null) {
            synchronized(this.transactions) {
                value = this.transactions.get();
                if (value == null) {
                    Map actualValue = this.readTxnsFromFile();
                    value = actualValue == null ? this.transactions : actualValue;
                    this.transactions.set(value);
                }
            }
        }

        return (Map)((Map)(value == this.transactions ? null : value));
    }
}


Lombok wrapped the data field in an *https://www.example.com/java-atomic-variables[AtomicReference].* これにより、transaction フィールドのアトミック更新が保証されることを指摘するのは興味深いことです。 getTransactions()メソッドは、transactions isnull.の場合、ファイルを確実に読み取るようにします。

クラス内から直接AtomicReference transactions fieldを使用することはお勧めしません。 It’s recommended to use the getTransactions() method for accessing the field.

このため、同じクラス,ToStringなどの別のLombokアノテーションを使用すると、フィールドに直接アクセスする代わりにgetTransactions()が使用されます。

4. バリュークラス/ DTO

複雑な「値」または「データ転送オブジェクト」として、ほとんどの場合、構築する不変のデータ構造の形式でデータ型を定義したい場合が多くあります一度も変更したくない。

ログイン操作が成功したことを表すクラスを設計します。 プロパティをスレッドセーフにアクセスできるように、すべてのフィールドをnullでなく、オブジェクトを不変にする必要があります。

public class LoginResult {

    private final Instant loginTs;

    private final String authToken;
    private final Duration tokenValidity;

    private final URL tokenRefreshUrl;

    // constructor taking every field and checking nulls

    // read-only accessor, not necessarily as get*() form
}

繰り返しになりますが、コメント付きセクション用に作成する必要のあるコードの量は、カプセル化する情報よりもはるかに多く、私たちにとって真の価値があります。 これを改善するためにLombokを再び使用できます。

@RequiredArgsConstructor
@Accessors(fluent = true) @Getter
public class LoginResult {

    private final @NonNull Instant loginTs;

    private final @NonNull String authToken;
    private final @NonNull Duration tokenValidity;

    private final @NonNull URL tokenRefreshUrl;

}

@RequiredArgsConstructorアノテーションを追加するだけで、宣言したとおりに、クラス内のすべての最終フィールドのコンストラクターを取得できます。 属性に@NonNullを追加すると、コンストラクターはnull可能性をチェックし、それに応じてNullPointerExceptionsをスローします。 これは、フィールドが非ファイナルであり、それらに@Setterを追加した場合にも発生します。

プロパティに古いget*()フォームを退屈させたくないですか? この例では@Accessors(fluent=true)を追加したため、「getters」はプロパティと同じメソッド名を持ちます。getAuthToken()は単にauthToken()になります。

この「流れるような」フォームは、属性セッターの非最終フィールドに適用され、連鎖呼び出しも許可します。

// Imagine fields were no longer final now
return new LoginResult()
  .loginTs(Instant.now())
  .authToken("asdasd")
  . // and so on

5. コアJavaボイラープレート

維持する必要のあるコードを記述してしまうもう1つの状況は、toString()equals()、およびhashCode()メソッドを生成するときです。 IDEは、クラス属性に関してこれらを自動生成するためのテンプレートを支援しようとします。

他のLombokクラスレベルのアノテーションを使用して、これを自動化できます。

  • @ToString:すべてのクラス属性を含むtoString()メソッドを生成します。 データモデルを強化する際に、独自に作成して維持する必要はありません。

  • @EqualsAndHashCode:デフォルトでは、関連するすべてのフィールドを考慮し、very well though semanticsに従って、equals()メソッドとhashCode()メソッドの両方を生成します。

これらのジェネレーターには、非常に便利な構成オプションが付属しています。 たとえば、アノテーション付きクラスが階層の一部である場合は、callSuper=trueパラメータを使用するだけで、メソッドのコードを生成するときに親の結果が考慮されます。

詳細:UserのJPAエンティティの例に、このユーザーに関連付けられたイベントへの参照が含まれているとします。

@OneToMany(mappedBy = "user")
private List events;

@ToStringアノテーションを使用したという理由だけで、ユーザーのtoString()メソッドを呼び出すたびにイベントのリスト全体をダンプしたくありません。 問題ありません:@ToString(exclude = \{“events”})のようにパラメータ化するだけで、それは起こりません。 これは、たとえば、UserEventsにUserへの参照がある場合に、循環参照を回避するのにも役立ちます。

LoginResultの例では、クラス内の他の最終属性ではなく、トークン自体の観点から等価性とハッシュコードの計算を定義したい場合があります。 次に、@EqualsAndHashCode(of = \{“authToken”})のようなものを書くだけです。

ボーナス:これまでに確認したアノテーションの機能が気に入った場合は、@Data@Valueのアノテーションが、それらのセットがクラスに適用されたかのように動作するので、それらを調べることをお勧めします。 結局のところ、これらの議論された使用法は、多くの場合非常に一般的にまとめられています。

6. ビルダーパターン

以下は、REST APIクライアントのサンプル構成クラスを作成します。

public class ApiClientConfiguration {

    private String host;
    private int port;
    private boolean useHttps;

    private long connectTimeout;
    private long readTimeout;

    private String username;
    private String password;

    // Whatever other options you may thing.

    // Empty constructor? All combinations?

    // getters... and setters?
}

クラスのデフォルトの空のコンストラクターを使用し、すべてのフィールドにセッターメソッドを提供することに基づいた初期アプローチがあります。 ただし、理想的には、構成が構築(インスタンス化)された後は、構成が再setにならないようにして、事実上不変にする必要があります。 したがって、セッターを避けたいのですが、そのような潜在的に長い引数コンストラクタを書くことはアンチパターンです。

代わりに、ApiClientConfigurationに@Builderアノテーションを追加するだけで、builderパターンを生成するようにツールに指示し、余分なBuilderクラスと関連する流暢なセッターのようなメソッドを記述できなくなります。 。

@Builder
public class ApiClientConfiguration {

    // ... everything else remains the same

}

上記のクラス定義をそのままにしておくと(コンストラクターもセッターも宣言しない+@Builder)、最終的に次のように使用できます。

ApiClientConfiguration config =
    ApiClientConfiguration.builder()
        .host("api.server.com")
        .port(443)
        .useHttps(true)
        .connectTimeout(15_000L)
        .readTimeout(5_000L)
        .username("myusername")
        .password("secret")
    .build();

7. チェックされた例外の負担

多くのJavaAPIは、チェックされた多数の例外をスローできるように設計されており、クライアントコードはcatchに強制されるか、throwsに宣言されます。 発生しないことがわかっているこれらの例外をこのようなものに変えたことは何回ありますか?

public String resourceAsString() {
    try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        return br.lines().collect(Collectors.joining("\n"));
    } catch (IOException | UnsupportedCharsetException ex) {
        // If this ever happens, then its a bug.
        throw new RuntimeException(ex); <--- encapsulate into a Runtime ex.
    }
}

コンパイラが他の方法では満足できないためにこのコードパターンを回避したい場合(そして、結局のところ、チェックされたエラーは発生しません)、適切な名前の@SneakyThrowsを使用します。

@SneakyThrows
public String resourceAsString() {
    try (InputStream is = this.getClass().getResourceAsStream("sure_in_my_jar.txt")) {
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        return br.lines().collect(Collectors.joining("\n"));
    }
}

8. リソースが解放されていることを確認します

Java 7では、try-with-resourcesブロックが導入され、java.langAutoCloseableを実装するインスタンスによって保持されているリソースが終了時に解放されるようになっています。

Lombokは、これを実現するための代替方法を提供し、@Cleanupを介してより柔軟に提供します。 確認するリソースが解放されているローカル変数に使用します。 特定のインターフェースを実装する必要はありません。close()メソッドが呼び出されるだけです。

@Cleanup InputStream is = this.getClass().getResourceAsStream("res.txt");

リリース方法に別の名前がありますか? 問題ありません、注釈をカスタマイズするだけです:

@Cleanup("dispose") JFrame mainFrame = new JFrame("Main Window");

9. ロガーを取得するためにクラスに注釈を付ける

私たちの多くは、選択したフレームワークからLoggerのインスタンスを作成することにより、コードにロギングステートメントを控えめに追加します。 言う、SLF4J:

public class ApiClientConfiguration {

    private static Logger LOG = LoggerFactory.getLogger(ApiClientConfiguration.class);

    // LOG.debug(), LOG.info(), ...

}

これは非常に一般的なパターンであり、Lombokの開発者はこのパターンを単純化することを考えています。

@Slf4j // or: @Log @CommonsLog @Log4j @Log4j2 @XSlf4j
public class ApiClientConfiguration {

    // log.debug(), log.info(), ...

}

多くのlogging frameworksがサポートされており、もちろんインスタンス名やトピックなどをカスタマイズできます。

10. スレッドセーフなメソッドを書く

Javaでは、synchronizedキーワードを使用してクリティカルセクションを実装できます。 ただし、これは100%安全なアプローチではありません。他のクライアントコードも最終的にインスタンスで同期でき、予期しないデッドロックを引き起こす可能性があります。

ここで@Synchronizedが登場します。メソッド(インスタンスと静的の両方)にアノテーションを付けると、実装がロックに使用する自動生成された非公開のプライベートフィールドが取得されます。

@Synchronized
public /* better than: synchronized */ void putValueInCache(String key, Object value) {
    // whatever here will be thread-safe code
}

11. オブジェクト構成の自動化

Javaには、「お気に入りの構成の継承」アプローチをスムーズにするための言語レベルの構造はありません。 他の言語には、これを実現するためのTraitsMixinsなどの組み込みの概念があります。

Lombokの@Delegateは、このプログラミングパターンを使用する場合に非常に便利です。 例を考えてみましょう:

  • UsersとCustomersで、名前と電話番号のいくつかの共通属性を共有する必要があります

  • これらのフィールドにインターフェースとアダプタークラスの両方を定義します

  • モデルにインターフェースとアダプターへの@Delegateを実装させ、連絡先情報を効果的にcomposingします。

まず、インターフェースを定義しましょう。

public interface HasContactInformation {

    String getFirstName();
    void setFirstName(String firstName);

    String getFullName();

    String getLastName();
    void setLastName(String lastName);

    String getPhoneNr();
    void setPhoneNr(String phoneNr);

}

そして今、supportクラスとしてのアダプター:

@Data
public class ContactInformationSupport implements HasContactInformation {

    private String firstName;
    private String lastName;
    private String phoneNr;

    @Override
    public String getFullName() {
        return getFirstName() + " " + getLastName();
    }
}

興味深い部分が登場しました。連絡先情報を両方のモデルクラスに簡単に作成できることを確認してください。

public class User implements HasContactInformation {

    // Whichever other User-specific attributes

    @Delegate(types = {HasContactInformation.class})
    private final ContactInformationSupport contactInformation =
            new ContactInformationSupport();

    // User itself will implement all contact information by delegation

}

Customerの場合は非常に似ているため、簡潔にするためにサンプルを省略します。

12. Lombokをロールバックしますか?

簡単な答え:まったくそうではありません。

プロジェクトの1つでLombokを使用する可能性がありますが、後でその決定をロールバックしたい場合があります。 その場合、おそらく多数のクラスに注釈が付けられます…何ができますか?

私はこれを本当に後悔していませんが、誰があなた、あなたのチーム、またはあなたの組織を知っていますか。 これらのケースでは、同じプロジェクトのdelombokツールのおかげでカバーされます。

コードをdelombok-ingすることで、Lombokが構築したバイトコードとまったく同じ機能を備えた自動生成されたJavaソースコードを取得できます。 したがって、元の注釈付きコードをこれらの新しいdelombokedファイルに置き換えるだけで、依存しなくなります。

これはintegrate in your buildできることであり、過去にこれを行って、生成されたコードを調べたり、Lombokを他のJavaソースコードベースのツールと統合したりしました。

13. 結論

この記事では紹介していない機能が他にもいくつかあります。詳細と使用例については、feature overviewを詳しく調べることをお勧めします。

また、これまでに示したほとんどの関数には、名前付けなどのチームプラクティスに最も準拠したものをツールで生成するために便利なカスタマイズオプションがいくつかあります。 利用可能な組み込みのconfiguration systemもそれを支援する可能性があります。

LombokにJava開発ツールセットを導入する機会を与える動機付けを見つけたことを願っています。 試して、生産性を高めてください!

サンプルコードはGitHub projectにあります。