SpringFrameworkの最も一般的な間違いトップ10

公開: 2022-03-11

Springは間違いなく最も人気のあるJavaフレームワークの1つであり、飼いならすための強力な獣でもあります。 その基本的な概念はかなり理解しやすいものですが、強力なSpring開発者になるには、ある程度の時間と労力が必要です。

この記事では、特にWebアプリケーションとSpring Bootに向けられた、Springでよくある間違いのいくつかを取り上げます。 Spring BootのWebサイトに記載されているように、Spring Bootは、本番環境に対応したアプリケーションを構築する方法について意見を述べています。そのため、この記事では、そのビューを模倣し、標準のSpringBootWebアプリケーション開発にうまく組み込まれるヒントの概要を説明します。

Spring Bootにあまり詳しくないが、言及されていることのいくつかを試してみたい場合は、この記事に付随するGitHubリポジトリを作成しました。 記事の途中で迷子になったと感じた場合は、リポジトリのクローンを作成し、ローカルマシンでコードを試してみることをお勧めします。

よくある間違い#1:レベルが低すぎる

「ここで発明されていない」症候群はソフトウェア開発の世界では非常に一般的であるため、私たちはこのよくある間違いでそれを打ち負かしています。 一般的に使用されるコードの一部を定期的に書き直すなどの症状や、多くの開発者が苦しんでいるようです。

特定のライブラリの内部とその実装を理解することは、ほとんどの場合、適切で必要です(また、優れた学習プロセスになる可能性もあります)が、同じ低レベルの実装に絶えず取り組むことは、ソフトウェアエンジニアとしての開発にとって有害で​​す。詳細。 Springなどの抽象化とフレームワークが存在するのには理由があります。これは、繰り返しの手作業から正確に分離し、ドメインオブジェクトとビジネスロジックなどのより高いレベルの詳細に集中できるようにするためです。

したがって、抽象化を受け入れます。次に特定の問題に直面したときは、最初にクイック検索を実行して、その問題を解決するライブラリがすでにSpringに統合されているかどうかを判断します。 今日では、適切な既存のソリューションが見つかる可能性があります。 便利なライブラリの例として、この記事の残りの部分の例ではProjectLombokアノテーションを使用します。 Lombokはボイラープレートコードジェネレーターとして使用されており、あなたの中にいる怠惰な開発者は、ライブラリに精通するのに問題がないことを願っています。 例として、Lombokで「標準のJavaBean」がどのように見えるかを確認してください。

 @Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; }

ご想像のとおり、上記のコードは次のようにコンパイルされます。

 public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } }

ただし、IDEでLombokを使用する場合は、プラグインをインストールする必要がある可能性が高いことに注意してください。 IntelliJIDEAのプラグインのバージョンはここにあります。

よくある間違い#2:「漏れ」内部

内部構造を公開することは、サービスデザインに柔軟性を持たせず、その結果、不適切なコーディング慣行を助長するため、決して良い考えではありません。 「リーク」内部は、特定のAPIエンドポイントからデータベース構造にアクセスできるようにすることで明らかになります。 例として、次のPOJO(「PlainOld Java Object」)がデータベース内のテーブルを表すとします。

 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } }

TopTalentEntityデータにアクセスする必要があるエンドポイントが存在するとします。 TopTalentEntityインスタンスを返すこともありますが、より柔軟なソリューションは、APIエンドポイントでTopTalentEntityデータを表す新しいクラスを作成することです。

 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }

そうすれば、データベースのバックエンドに変更を加えるために、サービスレイヤーに追加の変更を加える必要はありません。 ユーザーのパスワードハッシュをデータベースに保存するためにTopTalentEntityに「password」フィールドを追加した場合にどうなるかを考えてくださいTopTalentDataなどのコネクタがないと、サービスのフロントエンドを変更し忘れると、非常に望ましくない秘密情報が誤って公開される可能性があります。 !!

よくある間違い#3:関心の分離の欠如

アプリケーションが成長するにつれて、コード編成はますます重要な問題になり始めます。 皮肉なことに、優れたソフトウェアエンジニアリングの原則のほとんどは、特にアプリケーションアーキテクチャの設計についてあまり考慮されていない場合に、大規模に崩壊し始めます。 開発者が陥りがちな最も一般的な間違いの1つは、コードの懸念を混合することであり、それは非常に簡単です。

通常、関心の分離を破るのは、新しい機能を既存のクラスに「ダンプ」することです。 もちろん、これは優れた短期的な解決策です(初心者にとっては、入力が少なくて済みます)が、テスト中、メンテナンス中、またはその間のどこかで、必然的に問題になります。 リポジトリからTopTalentDataを返す次のコントローラーについて考えてみます。

 @RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

最初は、このコードに特に問題はないように思われるかもしれません。 TopTalentEntityインスタンスから取得されているTopTalentDataのリストを提供します。 ただし、よく見ると、 TopTalentControllerがここで実行していることが実際にいくつかあることがわかります。 つまり、リクエストを特定のエンドポイントにマッピングし、リポジトリからデータを取得し、 TopTalentRepositoryから受信したエンティティを別の形式に変換します。 「よりクリーンな」ソリューションは、これらの懸念を独自のクラスに分離することです。 次のようになります。

 @RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }

この階層の追加の利点は、クラス名を調べるだけで機能がどこにあるかを判別できることです。 さらに、テスト中に、必要に応じて、任意のクラスをモック実装に簡単に置き換えることができます。

よくある間違い#4:矛盾と不十分なエラー処理

一貫性のトピックは、必ずしもSpring(またはJava)に限定されているわけではありませんが、Springプロジェクトで作業する際に考慮すべき重要な側面です。 コーディングスタイルは議論の余地がありますが(通常、チーム内または会社全体の合意の問題です)、共通の標準を持つことは生産性の大きな助けになることがわかります。 これは特に複数人のチームに当てはまります。 一貫性により、手持ちに多くのリソースを費やしたり、さまざまなクラスの責任に関する長い説明を提供したりすることなく、ハンドオフを実行できます。

さまざまな構成ファイル、サービス、およびコントローラーを備えたSpringプロジェクトについて考えてみます。 それらに名前を付ける際に意味的に一貫していることは、新しい開発者がコードを回避する方法を管理できる、簡単に検索可能な構造を作成します。 たとえば、構成クラスに構成サフィックスを追加し、サービスにサービスサフィックスを追加し、コントローラーにコントローラーサフィックスを追加します。

一貫性のトピックと密接に関連しているため、サーバー側でのエラー処理は特に強調する価値があります。 不適切に記述されたAPIからの例外応答を処理する必要があった場合は、おそらくその理由をご存知でしょう。例外を適切に解析するのは面倒であり、そもそもそれらの例外が発生した理由を判断するのはさらに面倒です。

API開発者は、理想的には、ユーザー向けのすべてのエンドポイントをカバーし、それらを一般的なエラー形式に変換する必要があります。 これは通常、a)「500Internal Server Error」メッセージを返す、またはb)スタックトレースをユーザーにダンプするだけのコップアウトソリューションではなく、一般的なエラーコードと説明を使用することを意味します(実際には絶対に避けてください)。クライアント側での処理が難しいことに加えて、内部が公開されるためです)。

一般的なエラー応答形式の例は次のとおりです。

 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }

これに似たものは、最も一般的なAPIで一般的に見られ、簡単かつ体系的に文書化できるため、うまく機能する傾向があります。 例外をこの形式に変換するには、メソッドに@ExceptionHandlerアノテーションを指定します(アノテーションの例は、よくある間違い#6にあります)。

よくある間違い#5:マルチスレッドの不適切な処理

デスクトップアプリとWebアプリのどちらで発生するか、Springであるかどうかに関係なく、マルチスレッド化は難しい問題になる可能性があります。 プログラムの並列実行によって引き起こされる問題は、神経質になりにくいものであり、デバッグが非常に難しい場合があります。実際、問題の性質上、並列実行の問題に対処していることに気付いたら、おそらく次のようになります。デバッガーを完全に放棄し、根本的なエラーの原因が見つかるまで「手作業で」コードを検査する必要があります。 残念ながら、そのような問題を解決するためのCookieカッターソリューションは存在しません。 特定のケースに応じて、状況を評価してから、最善と思われる角度から問題を攻撃する必要があります。

もちろん、理想的には、マルチスレッドのバグを完全に回避したいと思うでしょう。 繰り返しになりますが、これを行うための万能のアプローチは存在しませんが、マルチスレッドエラーをデバッグおよび防止するためのいくつかの実用的な考慮事項を次に示します。

グローバルステートを回避する

まず、常に「グローバル状態」の問題を覚えておいてください。 マルチスレッドアプリケーションを作成している場合は、グローバルに変更可能なものはすべて綿密に監視し、可能であれば完全に削除する必要があります。 グローバル変数を変更可能なままにする必要がある理由がある場合は、同期を慎重に採用し、アプリケーションのパフォーマンスを追跡して、新しく導入された待機期間が原因でアプリケーションが遅くならないことを確認します。

可変性を避ける

これは関数型プログラミングから直接得られたものであり、OOPに適合しており、クラスの可変性と状態の変化は避けるべきであると述べています。 つまり、これは、セッターメソッドを前述し、すべてのモデルクラスにプライベートfinalフィールドを持つことを意味します。 それらの値が変更されるのは、構築中のみです。 このようにして、競合の問題が発生せず、オブジェクトのプロパティにアクセスすると常に正しい値が提供されることを確認できます。

重要なデータをログに記録する

アプリケーションが問題を引き起こす可能性のある場所を評価し、すべての重要なデータを先制的にログに記録します。 エラーが発生した場合は、どのリクエストを受信したかを示す情報を入手し、アプリケーションが誤動作した理由をよりよく理解できるようになります。 ロギングによって追加のファイルI/Oが発生するため、アプリケーションのパフォーマンスに深刻な影響を与える可能性があるため、悪用しないでください。

既存の実装を再利用する

独自のスレッドを生成する必要がある場合(たとえば、さまざまなサービスへの非同期リクエストを作成する場合)は、独自のソリューションを作成するのではなく、既存の安全な実装を再利用してください。 これは、ほとんどの場合、ExecutorServicesとJava8のきちんとした機能スタイルのCompletableFuturesをスレッド作成に利用することを意味します。 Springでは、DeferredResultクラスを介した非同期リクエスト処理も可能です。

よくある間違い#6:注釈ベースの検証を採用していない

以前のTopTalentサービスには、新しいTopTalentを追加するためのエンドポイントが必要だと想像してみてください。 さらに、いくつかの本当に正当な理由のために、すべての新しい名前は正確に10文字の長さである必要があるとしましょう。 これを実行するための1つの方法は、次のとおりです。

 @RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); }

ただし、上記は(不十分に構築されていることに加えて)実際には「クリーンな」ソリューションではありません。 複数のタイプの有効性(つまり、 TopTalentDataがnullないこと、 TopTalentData.nameがnullないこと、 TopTalentData.nameの長さが10文字であること)を確認し、データが無効な場合は例外をスローします。 。

これは、SpringでHibernateバリデーターを使用することではるかにクリーンに実行できます。 まず、検証をサポートするためにaddTopTalentメソッドをリファクタリングしましょう。

 @RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }

さらに、 TopTalentDataクラスで検証するプロパティを指定する必要があります。

 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; }

これで、Springはリクエストをインターセプトし、メソッドが呼び出される前に検証します。追加の手動テストを採用する必要はありません。

同じことを達成できたもう1つの方法は、独自の注釈を作成することです。 通常、カスタムアノテーションを使用するのは、ニーズがHibernateの組み込みの制約セットを超える場合のみですが、この例では、@Lengthが存在しないと仮定しましょう。 2つの追加クラスを作成して、文字列の長さをチェックするバリデーターを作成します。1つは検証用で、もう1つはプロパティに注釈を付けるためです。

 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } }

これらの場合、関心の分離に関するベストプラクティスでは、プロパティがnullの場合は有効としてマークし( isValidメソッド内でs == null )、それが追加の要件である場合は@NotNullアノテーションを使用する必要があることに注意してください。財産:

 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }

よくある間違い#7 :(まだ)XMLベースの構成を使用する

以前のバージョンのSpringではXMLが必要でしたが、現在、ほとんどの構成はJavaコード/アノテーションを介して排他的に実行できます。 XML構成は、追加の不要な定型コードを装うだけです。

この記事(および付随するGitHubリポジトリ)は、Springを構成するためにアノテーションを使用し、ルートパッケージには@SpringBootApplicationコンポジットアノテーションが付けられているため、SpringはどのBeanをワイヤリングする必要があるかを認識しています。

 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

複合アノテーション(Springのドキュメントで詳細を確認できます)は、Beanを取得するためにスキャンする必要があるパッケージに関するヒントをSpringに提供するだけです。具体的なケースでは、これは、最上位(co.kukurin)パッケージの下で次のものが使用されることを意味します。配線用:

  • @ComponentTopTalentConverterMyAnnotationValidator
  • @RestControllerTopTalentController
  • @RepositoryTopTalentRepository
  • @ServiceTopTalentService )クラス

追加@Configurationアノテーションが付けられたクラスがある場合、それらはJavaベースの構成についてもチェックされます。

よくある間違い#8:プロファイルを忘れる

サーバー開発でよく発生する問題は、さまざまな構成タイプ(通常は実稼働構成と開発構成)を区別することです。 テストからアプリケーションのデプロイに切り替えるたびにさまざまな構成エントリを手動で置き換える代わりに、プロファイルを使用する方が効率的な方法です。

ローカル開発にインメモリデータベースを使用していて、本番環境にMySQLデータベースがある場合を考えてみます。 これは、本質的に、2つのそれぞれにアクセスするために異なるURLと(うまくいけば)異なるクレデンシャルを使用することを意味します。 これが2つの異なる構成ファイルでどのように行われるかを見てみましょう。

application.yamlファイル

# set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password:

application-dev.yamlファイル

spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2

おそらく、コードをいじっているときに本番データベースで誤ってアクションを実行したくないので、デフォルトのプロファイルをdevに設定するのが理にかなっています。 サーバーでは、JVMに-Dspring.profiles.active=prodパラメーターを指定することにより、構成プロファイルを手動でオーバーライドできます。 または、OSの環境変数を目的のデフォルトプロファイルに設定することもできます。

よくある間違い#9:依存性注入を受け入れられない

Springで依存性注入を適切に使用するということは、必要なすべての構成クラスをスキャンすることで、すべてのオブジェクトを相互に接続できるようにすることを意味します。 これは、関係を切り離すのに役立ち、テストも非常に簡単になります。 次のようなことを行うことで、クラスを密結合する代わりに、次のようにします。

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } }

Springが私たちのために配線を行うことを許可しています:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } }

Misko HeveryのGoogleトークでは、依存性注入の「理由」について詳しく説明しているので、代わりに実際にどのように使用されるかを見てみましょう。 関心の分離に関するセクション(よくある間違い#3)では、サービスとコントローラーのクラスを作成しました。 TopTalentServiceが正しく動作することを前提として、コントローラーをテストするとします。 別の構成クラスを提供することにより、実際のサービス実装の代わりにモックオブジェクトを挿入できます。

 @Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } }

次に、構成サプライヤとしてSampleUnitTestConfigを使用するようにSpringに指示することで、モックオブジェクトを挿入できます。

 @ContextConfiguration(classes = { SampleUnitTestConfig.class })

これにより、コンテキスト構成を使用してカスタムBeanを単体テストに注入できます。

よくある間違い#10:テストの欠如または不適切なテスト

ユニットテストのアイデアは長い間私たちにありましたが、多くの開発者はこれを「忘れる」か(特に必要がない場合)、単に後付けとして追加するようです。 テストはコードの正確さを検証するだけでなく、さまざまな状況でアプリケーションがどのように動作するかについてのドキュメントとしても機能するため、これは明らかに望ましくありません。

Webサービスをテストする場合、HTTPを介した通信では通常、SpringのDispatcherServletを呼び出して、実際のHttpServletRequestを受信したときに何が起こるかを確認する必要があるため、「純粋な」単体テストのみを実行することはめったにありません(統合テスト、検証、シリアル化の処理) 、など)。 REST Assuredは、MockMVCに加えてRESTサービスを簡単にテストするためのJava DSLであり、非常に洗練されたソリューションを提供することが証明されています。 依存性注入を伴う次のコードスニペットについて考えてみます。

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } }

SampleUnitTestConfigは、TopTalentServiceのモック実装をTopTalentServiceTopTalentControllerしますが、他のすべてのクラスは、Applicationクラスのパッケージをルートとするスキャンパッケージから推測される標準構成を使用してワイヤリングされます。 RestAssuredMockMvcは、軽量環境をセットアップし、 GETリクエストを/toptal/getエンドポイントに送信するために使用されます。

春の達人になる

Springは、簡単に使い始めることができる強力なフレームワークですが、完全に習得するには、ある程度の献身と時間が必要です。 時間をかけてフレームワークに慣れることで、長期的には生産性が確実に向上し、最終的にはよりクリーンなコードを記述して、より優れた開発者になることができます。

さらにリソースを探している場合は、Spring In Actionは、Springの主要なトピックの多くをカバーする優れた実践的な本です。