ProjectLombokを使用して無脂肪のJavaコードを作成する

公開: 2022-03-11

最近では、Javaコードを書くことを想像できないツールやライブラリがたくさんあります。 従来、GoogleGuavaやJodaTime(少なくともJava 8以前の時代)のようなものは、手元にある特定のドメインに関係なく、ほとんどの場合、プロジェクトに投入する依存関係の1つです。

Lombokは、典型的なライブラリ/フレームワークユーティリティではありませんが、私のPOMやGradleビルドでもその位置を占めるに値します。 ロンボクはかなり前から存在しており(2009年に最初にリリースされました)、それ以来かなり成熟しています。 しかし、私は常にそれがもっと注目に値するものだと感じていました。これは、Javaの自然な冗長性に対処するための素晴らしい方法です。

この投稿では、Lombokがこのような便利なツールである理由を探ります。

プロジェクトロンボク

Javaには、注目に値するソフトウェアであるJVM自体以外にも多くのことがあります。 Javaは成熟していてパフォーマンスが高く、その周りのコミュニティとエコシステムは巨大で活気に満ちています。

ただし、プログラミング言語として、Javaには独自の特異性と、かなり冗長にすることができる設計上の選択肢があります。 Java開発者が頻繁に使用する必要のあるいくつかの構造とクラスパターンを追加すると、一連の制約やフレームワークの規則に準拠する以外に、実際の価値をほとんどまたはまったくもたらさないコード行が多数発生することがよくあります。

ここでロンボクが活躍します。 これにより、作成する必要のある「ボイラープレート」コードの量を大幅に減らすことができます。 ロンボクのクリエイターは非常に賢い人で、確かにユーモアのセンスがあります。過去の会議で彼らが作成したこのイントロを見逃すことはできません。

ロンボクがその魔法をどのように行うかといくつかの使用例を見てみましょう。

ロンボクのしくみ

Lombokは、コンパイル時にクラスにコードを「追加」するアノテーションプロセッサとして機能します。 アノテーション処理は、バージョン5でJavaコンパイラに追加された機能です。ユーザーは、アノテーションプロセッサ(自分で作成したもの、またはLombokなどのサードパーティの依存関係を介して作成したもの)をビルドクラスパスに入れることができます。 次に、コンパイルプロセスが進行しているときに、コンパイラがアノテーションを見つけるたびに、「ねえ、この@Annotationに関心のあるクラスパスの誰か?」と尋ねます。 手を挙げているプロセッサの場合、コンパイラは、コンパイルコンテキストとともに制御をプロセッサに転送し、処理します。

おそらく、注釈プロセッサの最も一般的なケースは、新しいソースファイルを生成するか、ある種のコンパイル時チェックを実行することです。

Lombokは、実際にはこれらのカテゴリに分類されません。Lombokが行うことは、コードを表すために使用されるコンパイラデータ構造を変更することです。 つまり、その抽象構文木(AST)です。 コンパイラのASTを変更することにより、Lombokは最終的なバイトコード生成自体を間接的に変更します。

この珍しい、そしてかなり煩わしいアプローチは、伝統的にロンボクをいくらかハックと見なされる結果になりました。 私自身、この特徴づけにある程度同意しますが、これを悪い意味で見るのではなく、ロンボクを「賢く、技術的に価値のある、独創的な代替手段」と見なします。

それでも、それをハックと見なし、この理由でLombokを使用しない開発者がいます。 それは理解できますが、私の経験では、ロンボクの生産性のメリットはこれらの懸念のどれよりも重要です。 私は何年もの間、それを生産プロジェクトに喜んで使用しています。

詳細に入る前に、私がプロジェクトでロンボクを使用することを特に重視している2つの理由を要約したいと思います。

  1. Lombokは、私のコードをクリーン、簡潔、そして要点に保つのに役立ちます。 私のLombok注釈付きクラスは非常に表現力豊かであり、インターネット上のすべての人が必ずしも同意するわけではありませんが、注釈付きコードは一般的に非常に意図を明らかにするものであると思います。
  2. プロジェクトを開始してドメインモデルを考えるとき、私は非常に進行中の作業であるクラスを書くことから始める傾向があり、さらに考えてそれらを洗練するにつれて繰り返し変更します。 これらの初期段階では、Lombokは、生成されたボイラープレートコードを移動したり変換したりする必要がないため、より速く移動できます。

Beanパターンと共通オブジェクトメソッド

私たちが使用するJavaツールとフレームワークの多くは、Beanパターンに依存しています。 Java Beansは、デフォルトのゼロ引数コンストラクター(および場合によっては他のバージョン)を持つシリアル化可能なクラスであり、通常はプライベートフィールドに基づくゲッターとセッターを介して状態を公開します。 たとえば、JPAや、JAXBやJacksonなどのシリアル化フレームワークを使用する場合などに、これらをたくさん記述します。

最大5つの属性(プロパティ)を保持するこのユーザーBeanについて考えてみます。このユーザーには、すべての属性のコンストラクター、意味のある文字列表現を追加し、電子メールフィールドに関して同等性/ハッシュを定義する必要があります。

 public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }

ここでは簡潔にするために、すべてのメソッドの実際の実装を含めるのではなく、メソッドと実際の実装に使用されたコードの行数をリストしたコメントを提供しました。 その定型コードは、このクラスのコードの90%以上になります。

さらに、後でemailemailAddressに変更したり、 registrationTsInstantではなくDateにしたりしたい場合は、getなどを変更するために時間を割く必要があります(場合によってはIDEの助けを借りて)。 / setメソッドの名前とタイプ、ユーティリティコンストラクターの変更など。 繰り返しになりますが、私のコードに実用的なビジネス価値をもたらさない何かのための貴重な時間です。

ここでロンボクがどのように役立つか見てみましょう:

 import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; }

出来上がり! たくさんのlombok.*アノテーションを追加して、思い通りの結果を達成しました。 上記のリストは、まさにこれのために書く必要のあるすべてのコードです。 Lombokは私のコンパイラプロセスにフックしていて、私のためにすべてを生成しました(私のIDEの以下のスクリーンショットを参照してください)。

IDEスクリーンショット

お気づきのとおり、NetBeansインスペクター(これはIDEに関係なく発生します)は、Lombokがプロセスに追加したものを含め、コンパイルされたクラスのバイトコードを検出します。 ここで起こったことは非常に簡単です。

  • @Getter@Setterを使用して、すべての属性のゲッターとセッターを生成するようにLombokに指示しました。 これは、クラスレベルでアノテーションを使用したためです。 何をどの属性に対して生成するかを選択的に指定したい場合は、フィールド自体に注釈を付けることができます。
  • @NoArgsConstructor@AllArgsConstructorのおかげで、クラスのデフォルトの空のコンストラクターと、すべての属性の追加のコンストラクターを取得しました。
  • @ToStringアノテーションは、便利なtoString()メソッドを自動生成し、デフォルトで、名前のプレフィックスが付いたすべてのクラス属性を表示します。
  • 最後に、 equals()メソッドとhashCode()メソッドのペアを電子メールフィールドに関して定義するために、 @EqualsAndHashCodeを使用し、関連するフィールドのリスト(この場合は電子メールのみ)でパラメーター化しました。

Lombokアノテーションのカスタマイズ

これと同じ例に従って、Lombokのカスタマイズをいくつか使用してみましょう。

  • デフォルトのコンストラクターの可視性を下げたいのですが。 Beanに準拠するためにのみ必要なので、クラスのコンシューマーは、すべてのフィールドを受け取るコンストラクターのみを呼び出すことを期待しています。 これを強制するために、 AccessLevel.PACKAGEを使用して生成されたコンストラクターをカスタマイズしています。
  • コンストラクターでもセッターメソッドでも、フィールドにnull値が割り当てられないようにしたいと思います。 クラス属性に@NonNullアノテーションを付けるだけで十分です。 Lombokは、コンストラクターメソッドとセッターメソッドで適切な場合にNullPointerExceptionをスローするnullチェックを生成します。
  • password属性を追加しますが、セキュリティ上の理由からtoString()を呼び出すときに表示されたくありません。 これは、@ToStringの@ToString引数を介して実行されます。
  • ゲッターを介して状態を公開しても問題ありませんが、外部の可変性を制限したいと思います。 したがって、@ Getterはそのままにしておきますが、@ AccessLevel.PROTECTEDには@Getterを使用し@Setter
  • おそらく、 emailフィールドに何らかの制約を強制して、変更された場合に何らかのチェックが実行されるようにしたいと思います。 このために、私はsetEmail()メソッドを自分で実装するだけです。 Lombokは、既存のメソッドの生成を省略します。

Userクラスは次のようになります。

 import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }

一部のアノテーションでは、クラス属性をプレーン文字列として指定していることに注意してください。 たとえば、タイプミスや存在しないフィールドの参照を行うと、Lombokはコンパイルエラーをスローするため、問題ありません。 ロンボクで、私たちは安全です。

また、 setEmail()メソッドの場合と同様に、Lombokは問題なく、プログラマーがすでに実装しているメソッドに対しては何も生成しません。 これは、すべてのメソッドとコンストラクターに適用されます。

不変のデータ構造

Lombokが優れているもう1つのユースケースは、不変のデータ構造を作成する場合です。 これらは通常、「値型」と呼ばれます。 一部の言語にはこれらのサポートが組み込まれており、これを将来のJavaバージョンに組み込むための提案もあります。

ユーザーのログインアクションへの応答をモデル化するとします。 これは、インスタンス化してアプリケーションの他のレイヤーに戻したい種類のオブジェクトです(たとえば、HTTP応答の本文としてJSONシリアル化するため)。 このようなLoginResponseは変更可能である必要はまったくなく、Lombokはこれを簡潔に説明するのに役立ちます。 確かに、不変のデータ構造には他にも多くのユースケースがあります(他の品質の中でも特にマルチスレッドやキャッシュに適しています)が、この単純な例に固執しましょう:

 import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }

ここで注目に値する:

  • @RequiredArgsConstructorアノテーションが導入されました。 適切な名前が付けられており、まだ初期化されていないすべての最終フィールドのコンストラクターを生成します。
  • 以前に発行されたLoginResonseを再利用する場合(たとえば、「トークンの更新」操作を想像してください)、既存のインスタンスを変更するのではなく、それに基づいて新しいインスタンスを生成する必要があります。 。 @Witherアノテーションがどのように役立つかをここで確認してください。これは、指定している新しいインスタンス値を除いて、すべてのwith'edインスタンス値を持つLoginResponseの新しいインスタンスを作成するwithTokenExpiryTs(Instant tokenExpiryTs)メソッドを生成するようにLombokに指示します。 すべてのフィールドでこの動作を希望しますか? 代わりに、クラス宣言に@Witherを追加するだけです。

@Dataと@Value

これまでに説明した両方のユースケースは非常に一般的であるため、Lombokはそれらをさらに短くするためにいくつかのアノテーションを出荷します。 @Dataでクラスにアノテーションを付けると、Lombokは@Getter + @Setter + @ToString ToString+でアノテーションが付けられた場合と同じように動作します。 @EqualsAndHashCode + @RequiredArgsConstructor 。 同様に、 @Valueを使用すると、クラスが不変の(そして最終的な)クラスに変わります。これも、上記のリストで注釈が付けられているかのようになります。

ビルダーパターン

Userの例に戻ると、新しいインスタンスを作成する場合は、最大6つの引数を持つコンストラクターを使用する必要があります。 これはすでにかなり大きな数であり、クラスに属性をさらに追加するとさらに悪化します。 また、 lastNameフィールドとpayingCustomerフィールドにいくつかのデフォルト値を設定したいとします。

Lombokは非常に強力な@Builder機能を実装しており、Builderパターンを使用して新しいインスタンスを作成できます。 それをUserクラスに追加しましょう:

 import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }

これで、次のような新しいユーザーを流暢に作成できるようになりました。

 User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();

クラスが成長するにつれて、この構成がどれほど便利になるかを想像するのは簡単です。

委任/構成

「継承よりも構成を優先する」という非常に正しいルールに従いたい場合は、Javaが実際には役に立たないものであり、冗長性があります。 オブジェクトを作成する場合は、通常、委任メソッド呼び出しをあちこちに作成する必要があります。

Lombokは、 @Delegateを介してこのソリューションを提案します。 例を見てみましょう。

ContactInformationの新しい概念を導入したいとします。 これはUserが持っている情報であり、他のクラスにも持ってもらいたい場合があります。 次に、次のようなインターフェイスを介してこれをモデル化できます。

 public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }

次に、Lombokを使用して新しいContactInformationクラスを導入します。

 import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }

そして最後に、 UserをリファクタリングしてContactInformationで構成し、Lombokを使用して、インターフェイスコントラクトに一致する必要なすべての委任呼び出しを生成できます。

 import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }

HasContactInformationのメソッドの実装を作成する必要がなかったことに注意してください。これは、 ContactInformationインスタンスへの呼び出しを委任して、Lombokに実行するように指示しているものです。

また、委任されたインスタンスに外部からアクセスできるようにしたくないので、 @Getter(AccessLevel.NONE)を使用してカスタマイズし、ゲッターの生成を効果的に防止しています。

チェックされた例外

ご存知のとおり、Javaはチェックされた例外とチェックされていない例外を区別します。 これは、例外処理が結果として邪魔になることがあるため、言語に対する論争や批判の伝統的な情報源です。特に、チェックされた例外をスローするように設計されたAPIを処理する場合は、開発者がそれらをキャッチするか、メソッドを宣言する必要があります。それらを投げます。

この例を考えてみましょう。

 public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }

これは非常に一般的なパターンです。URLが整形式であることは確かですが、 URLコンストラクターがチェック済みの例外をスローするため、強制的にキャッチするか、メソッドを宣言してスローし、同じ状況で呼び出し元を出します。 これらのチェックされた例外をRuntimeException内にラップすることは、非常に拡張された方法です。 そして、コーディングするにつれて処理する必要のあるチェックされた例外の数が増えると、これはさらに悪化します。

つまり、これはまさにLombokの@SneakyThrowsの目的であり、メソッドでスローされる可能性のあるチェック済みの例外をチェックされていないものにラップし、面倒な作業から解放します。

 import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }

ロギング

このように、どのくらいの頻度でロガーインスタンスをクラスに追加しますか? (SLF4Jサンプル)

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

私はかなり推測するつもりです。 これを知って、Lombokの作成者は、カスタマイズ可能な名前(デフォルトはログ)でロガーインスタンスを作成するアノテーションを実装し、Javaプラットフォームで最も一般的なログフレームワークをサポートしました。 このように(ここでも、SLF4Jベース):

 import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }

生成されたコードに注釈を付ける

Lombokを使用してコードを生成する場合、実際にはメソッドを記述していないため、これらのメソッドに注釈を付ける機能が失われるように見える場合があります。 しかし、これは実際には真実ではありません。 むしろ、Lombokを使用すると、生成されたコードに注釈を付ける方法を、多少特殊な表記法を使用して伝えることができますが、真実は伝えられます。

依存性注入フレームワークの使用を対象としたこの例を考えてみましょう。コンストラクター注入を使用してUserRepositoryUserApiClientへの参照を取得するUserServiceクラスがあります。

 package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }

上記のサンプルは、生成されたコンストラクターに注釈を付ける方法を示しています。 Lombokを使用すると、生成されたメソッドとパラメーターに対しても同じことができます。

詳細はこちら

この投稿で説明されているロンボクの使用法は、私が長年にわたって最も有用であると個人的に見つけた機能に焦点を当てています。 ただし、利用可能な他の多くの機能とカスタマイズがあります。

ロンボクのドキュメントは非常に有益で徹底的です。 非常に詳細な説明と例を含む、すべての機能(注釈)専用のページがあります。 この投稿がおもしろいと思ったら、lombokとそのドキュメントを深く掘り下げて詳細を確認することをお勧めします。

プロジェクトサイトには、いくつかの異なるプログラミング環境でLombokを使用する方法が記載されています。 つまり、最も一般的なIDE(Eclipse、NetBeans、IntelliJ)がサポートされています。 私自身、プロジェクトごとに定期的に切り替えて、すべてにLombokを完璧に使用しています。

デロンボク!

デロンボクは「ロンボクツールチェーン」の一部であり、非常に便利です。 基本的には、Lombokアノテーション付きコードのJavaソースコードを生成し、Lombokで生成されたバイトコードと同じ操作を実行します。

これは、ロンボクの採用を検討している人にとっては素晴らしいオプションですが、まだよくわかりません。 自由に使い始めることができ、「ベンダーロックイン」は発生しません。 あなたまたはあなたのチームが後で選択を後悔した場合に備えて、いつでもdelombokを使用して対応するソースコードを生成し、Lombokに依存することなく使用できます。

Delombokは、Lombokが何をするかを正確に知るための優れたツールでもあります。 ビルドプロセスにプラグインする非常に簡単な方法があります。

代替案

Javaの世界には、ImmutablesやGoogle Auto Valueなど、コンパイル時にコードを強化または変更するためにアノテーションプロセッサを同様に使用する多くのツールがあります。 これら(そして確かに他のもの!)は、機能的にロンボクと重複しています。 私は特にImmutablesアプローチが大好きで、いくつかのプロジェクトでも使用しています。

Byte BuddyやJavassistなど、「バイトコード拡張」のための同様の機能を提供する他の優れたツールがあることも注目に値します。 ただし、これらは通常、実行時に機能し、この投稿の範囲を超えた独自の世界を構成します。

簡潔なJava

同じ問題のいくつかに対処するのに役立つ、より慣用的な、または言語レベルの設計アプローチを提供する、最新のJVMターゲット言語がいくつかあります。 確かに、Groovy、Scala、Kotlinは良い例です。 ただし、Javaのみのプロジェクトで作業している場合、Lombokは、プログラムをより簡潔で表現力豊かで保守しやすいものにするための優れたツールです。

関連: Dart言語:JavaとC#が十分にシャープでない場合