Springは間違いなく最も人気のあるJavaフレームワークの1つであり、飼いならすのが難しい獣でもあります。その基本は非常に理解しやすいですが、強力なSpring開発者になるには時間と労力がかかります。
この記事では、特にWebアプリケーションとSpring Bootを対象とした、Springで最も一般的なエラーのいくつかについて説明します。私たちがそれを見るように SpringBootのWebサイト 、これは 視点 既製の本番アプリケーションを構築する方法について説明します。この記事では、そのビジョンをエミュレートし、標準のSping BootWebアプリケーションの開発に組み込むことができるいくつかのヒントの概要を示します。
Spring Bootについて知らなくても、言及されていることのいくつかを試してみたい場合は、私が作成しました この記事に付属するGitHubリポジトリ 。この記事の途中で迷子になったと感じた場合は、リポジトリのクローンを作成し、ローカルマシンのコードをいじることをお勧めします。
私たちはこのよくある間違いから始めました。 ここで発明されていない 」は、ソフトウェア開発の世界では非常に一般的です。症状には、一般的に使用されるコードの断片を定期的に書き直すことが含まれ、多くの開発者がこれに苦しんでいるようです。
ライブラリの内部動作とその実装を理解することは、大部分が適切で必要ですが(また、優れた学習プロセスになる可能性もあります)、ソフトウェアエンジニアとしての開発にとって、同じ詳細な低レベルに絶えず取り組むことは有害です。レベルの実装。 Springのような抽象化とフレームワークが存在するのには理由があります。これは、反復的なワークブックから脱却し、ドメインオブジェクトとビジネスロジックなどのより高いレベルの詳細に集中できるようにするためです。
したがって、抽象化を受け入れます。次に特定の問題に直面したときは、最初にクイック検索を実行して、その問題を解決するライブラリがすでにSpringに組み込まれているかどうかを判断します。これらの時代には、適切な解決策がすでに整っていることがわかるでしょう。便利なライブラリの例として、からの注釈を使用します ロンボクプロジェクト この記事の残りの部分で。 Lombokはモデルコードジェネレーターとして使用されるため、あなたの中の怠惰な開発者は問題なくライブラリに慣れることができます。例として、「 Java 豆 標準 」Lombokの場合:
@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バージョンが見つかります ここに 。
内部構造を公開することは、サービスデザインに柔軟性を持たせず、その結果、不正なコードプラクティスを助長するため、決して良い考えではありません。内部作業の「フィルタリング」は、特定のAPI出口ポイントからデータベース構造にアクセスできるようにすることで明らかになります。例として、次のPOJO( 'Plain Old 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
のインスタンスを返すことは非常に魅力的ですが、より柔軟な解決策は、データを表す新しいクラスを作成することですTopTalentEntity
API出口点で:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
したがって、データベースのバックエンドに変更を加える場合、サービス層に追加の変更を加える必要はありません。 'password'フィールドがTopTalentEntity
に追加された場合にどうなるかを検討してください。関数を保存するには ハッシュ データベース内のユーザーのパスワードの-TopTalentData
のようなコネクタがないと、フロントエンドサービスの変更を忘れると、誤って望ましくない秘密情報が公開されてしまいます。
アプリケーションが成長するにつれて、コードの編成はすぐにはるかに重要になります。皮肉なことに、優れたソフトウェアエンジニアリングの原則の多くは、特にアプリケーションアーキテクチャの設計が十分に検討されていない場合に、規模が縮小し始めます。開発者が陥りがちな最も一般的な間違いの1つは、コードの懸念を最大化することであり、それは非常に簡単です。
アートでデザインとはどういう意味ですか
通常何が壊れますか 関心事の分離 これは、既存のクラスに新しい機能を「投げる」だけです。これは優れた短期的な解決策ですが(最初は入力が少なくて済みます)、テスト、メンテナンス中、またはその間のいずれかの時点で、必然的に後で問題になります。 TopTalentData
を返す次のコントローラーについて考えてみます。リポジトリから:
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping('/toptal/get') public List getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } }
一見すると、このコードには特に問題はないように見えるかもしれません。 TopTalentData
のリストを提供しますTopTalentEntity
のインスタンスから抽出されています。ただし、よく見ると、TopTalentController
;によって達成されていることがいくつかあることがわかります。これは主に、リクエストを特定の出口ポイントにマッピングし、リポジトリからデータを抽出し、TopTalentRepository
から受信したエンティティを変換することです。別の形式に。よりクリーンな解決策は、これらの懸念を独自のクラスに分けることです。次のようになります。
@RestController @RequestMapping('/toptal') @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping('/get') public List getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List 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()); } }
この階層の追加の利点は、クラスの名前を調べるだけで、機能がどこにあるかを判別できることです。また、試用期間中は、必要に応じて、任意のクラスをモック実装に簡単に置き換えることができます。
不整合の問題は、必ずしもSpring(またはJava)に固有のものではありませんが、Springプロジェクトで作業するときに考慮する重要な側面です。コーディングスタイルについては議論の余地がありますが(通常はチーム内または会社全体の合意の問題です)、共通の標準を持つことは生産性に大きな恩恵をもたらす可能性があります。これは、特にマルチパーソナルチームの場合に非常に当てはまります。一貫性により、さまざまなクラスの責任について多くのリソースを費やしたり、長い説明を提供したりすることなく、拒否を発生させることができます。
さまざまな構成ファイル、サービス、およびコントローラーを備えたSpringプロジェクトについて考えてみます。名前を付けるときに意味的に一貫していると、調査が容易な構造が作成されます。この構造では、どんなに新しい開発者でも、コード内を移動できます。たとえば、構成サフィックスを構成クラスに、サービスサフィックスをサービスに、コントローラーサフィックスをコントローラーに添付します。
一貫性の問題に密接に関連しているのはサーバー側のエラー処理であり、これは特に強調する価値があります。不適切に記述されたAPIからの例外応答を処理する必要があった場合は、おそらくその理由をご存知でしょう。例外を適切に解析するのは面倒な場合があり、さらに面倒なのは、それらの例外が最初に発生した理由を特定することです。
API開発者は、理想的には、ユーザーが直面するすべての出口点をカバーし、それらを一般的なエラー形式に変換する必要があります。これは通常、a)「500Internal Server Error」というメッセージを返す、またはb)ユーザーに解決策を検索させる(公開されるため、熱心に回避する必要があります)などの口実の解決策ではなく、一般的なエラーコードと説明を用意することを意味しますクライアントが処理するのが難しいことは別として、あなたの内面の仕事)。
一般的な応答形式エラーの例は次のとおりです。
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }
これに似たものは、最も人気のあるAPIによく見られ、簡単かつ体系的に文書化できるため、非常にうまく機能する傾向があります。この形式の例外は、注釈を付けることで変換できます@ExceptionHandler
メソッドへ(アノテーションの例はCommon Error#6にあります)。
デスクトップまたはWebアプリケーションのどちらにあるかに関係なく、Springでは、マルチスレッドの処理が難しい場合があります。並列プログラムの実行によって引き起こされる問題は、ストレスが多く、とらえどころがなく、デバッグが非常に困難です。実際、問題の性質を考えると、並列実行の問題に対処していることに気付いたら、おそらくデバッガーを完全に続行する必要があります。 、ルートエラーの原因が見つかるまで、コードを「手作業で」検査します。残念ながら、そのような問題を解決するための通常の解決策は存在しません。特定のケースに応じて、状況を調査してから、最も適切と思われる角度から問題に取り組む必要があります。
もちろん、理想的にはマルチスレッドのバグを完全に回避したいと思うでしょう。繰り返しになりますが、標準的なアプローチは存在しませんが、マルチスレッドエラーをデバッグおよび防止するための実用的な考慮事項を次に示します。
まず、常に「グローバル状態」の問題を覚えておいてください。マルチスレッドアプリケーションを作成している場合は、変更可能なものはすべて厳密に監視し、可能であれば完全に削除する必要があります。グローバル変数を変更可能なままにする必要がある理由がある場合は、 同期 アプリケーションのパフォーマンスを追跡して、新しい待機期間が原因でアプリケーションがクラッシュしていないことを確認します。
これは 関数型プログラミング そして、OOPに適合して、可変性のクラスと状態の変化は避けるべきであると言っています。つまり、これは、メソッドの固定を続行し、すべてのモデルクラスにプライベートエンドフィールドを含めることを意味します。その値を変更できるのは、構築中のみです。このようにして、競合の問題が発生しないようにし、それにアクセスするオブジェクトプロパティが常に正しい値を提供するようにすることができます。
アプリケーションが問題を引き起こす可能性のある場所を評価し、すべての重要なデータを予防的に記録します。エラーが発生した場合は、どのリクエストを受信したかを示す情報を入手できれば幸いです。また、アプリケーションが失敗した理由をよりよく理解できます。繰り返しになりますが、レジストリは追加のファイルI / Oを導入するため、アプリケーションのパフォーマンスに深刻な影響を与える可能性があるため、悪用しないでください。
独自のスレッドを生成する必要がある場合(たとえば、さまざまなサービスに対して非同期要求を行う場合)、独自のソリューションを作成する代わりに、既存の安全な実装を再利用します。これは主にあなたが使用する必要があることを意味します ExecutorServices Y CompletableFutures スレッド作成のためのクリーンで機能的なスタイルを備えたJava8。 Springでは、クラスを介して非同期リクエストを処理することもできます DeferredResult 。
上記のTopTalentサービスでは、新しいTopTalentを追加するための出口ポイントが必要であると想像してみてください。さらに、いくつかの非常に正当な理由で、新しい名前はそれぞれ正確に10文字の長さである必要があるとしましょう。これを行うための有効な方法は次のとおりです。
@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ではなく、 Y そのTopTalentData.name
nullではない、 Y そのTopTalentData.name
10文字)、およびデータが無効な場合は例外をスローします。
これは、を使用することではるかにクリーンに行うことができます 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つの追加クラスを作成して、文字列の長さをチェックするバリデーターを作成します。
@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[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) }
このような場合、関心の分離のベストプラクティスでは、プロパティがnull(s == null
isValid
内)の場合に有効としてマークしてから、アノテーション@NotNull
を使用する必要があることに注意してください。プロパティの追加要件である場合:
public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }
以前のバージョンのSpringではXMLが必要でしたが、現在、ほとんどの構成はJavaコード/アノテーションを介して排他的に実行できます。 XML構成は、単なる追加の不要な標準テキストコードです。
この記事(およびそのコンパニオンGitHubリポジトリ)は、アノテーションを使用してSpringを構成し、どのアノテーションを使用しているかを認識しています。 豆 ルートパッケージには次のように複合アノテーション@SpringBootApplication
が付けられているため、転送する必要があります。
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
複合アノテーション(これについて詳しくは、 Springドキュメント )パッケージをスキャンして削除する必要があるというヒントをSpringに提供するだけです 豆 。私たちの特定のケースでは、これは上記のパッケージ(co.kukurin)の下の以下が転送に使用されることを意味します:
@Component
(TopTalentConverter
、MyAnnotationValidator
)@RestController
(TopTalentController
)@Repository
(TopTalentRepository
)@Service
(TopTalentService
)クラスアノテーションクラスがある場合@Configuration
追加のものは、Java基本構成によってチェックされます。
サーバー開発における一般的な問題は、さまざまなタイプの構成、通常は開発構成と実稼働構成を区別することです。アプリケーションのテストからデプロイに移るたびに複数の構成エントリを手動でオーバーライドするよりも、プロファイルを使用する方が便利な方法です。
ローカル開発にメモリ組み込みデータベースを使用していて、本番環境にMySQLデータベースがある場合を考えてみます。これは基本的に、それぞれにアクセスするために異なるURLと(うまくいけば)異なる資格情報を使用することを意味します。これを2つの異なる構成ファイルで行う方法を見てみましょう。
# 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:
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2
コードのレビュー中に本番データベースで誤ってアクションを実行したくないと想定されるため、開発時にデフォルトのプロファイルを設定することは理にかなっています。サーバーでは、パラメーター-Dspring.profiles.active=prod
を指定することにより、構成プロファイルを手動でオーバーライドできます。 JVMで。または、OS環境変数を目的のデフォルトプロファイルに設定することもできます。
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 deGoogleトーク 依存性注入の「理由」を詳しく説明しているので、実際にどのように使用されるかを見てみましょう。関心の分離のセクション(よくある間違い#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; } }
次に、SpringにSampleUnitTestConfig
を使用するように指示することで、ダミーオブジェクトを挿入できます。構成プロバイダーとして:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })
これにより、コンテキスト設定を使用して 豆 テストユニットにオーダーメイド。
ユニットテストのアイデアは長い間私たちにありましたが、多くの開発者はこれを「忘れている」ようです(特にそれが彼らのものでない場合) が必要 )、または重要でないものとして追加するだけです。テストはコードの正確さを検証するだけでなく、さまざまな状況でアプリケーションがどのように動作するかについてのドキュメントとしても機能するため、これは明らかに私たちが望んでいないことです。
Webサービスをテストする場合、HTTP通信では通常、DispatcherServlet
を呼び出す必要があるため、純粋に「純粋な」単体テストを行うことはめったにありません。春のそしてHttpServletRequest
実際に受け取られます(それを証明する 統合 、検証の処理、公開のリリースなど)。 RESTは保証します 、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
の偽の実装を渡しますa TopTalentController
他のすべてのクラスは、Applicationクラスパッケージに埋め込まれたスキャンパッケージによって推測される標準構成を使用して転送されます。 RestAssuredMockMvc
軽量環境を確立するために使用され、リクエストを送信しますGET
出口点へ/toptal/get
。
Springは非常に強力なフレームワークであり、開始は簡単ですが、完全に習得するにはある程度の献身と時間が必要です。時間をかけてフレームワークに慣れることで、長期的には生産性が向上し、最終的にはよりクリーンなコードを記述して、より優れた開発者になることができます。
このトピックに関するその他の資料をお探しの場合は、 Spring In Action 春の重要なトピックの多くをカバーする良い本です。