Springは間違いなく最も人気のあるJavaフレームワークの1つであり、飼いならす強力な獣でもあります。その基本的な概念はかなり理解しやすいですが、強力なSpring開発者になるには、ある程度の時間と労力が必要です。
この記事では、特にWebアプリケーションとSpring Bootに向けられた、Springでよくある間違いのいくつかを取り上げます。なので SpringBootのウェブサイト 状態、SpringBootは 意見のある見解 本番環境に対応したアプリケーションを構築する方法について説明します。この記事では、そのビューを模倣し、標準のSpring BootWebアプリケーション開発にうまく組み込まれるいくつかのヒントの概要を説明します。
Spring Bootにあまり詳しくないが、言及されていることのいくつかを試してみたい場合のために、私は作成しました この記事に付随するGitHubリポジトリ 。記事の途中で迷子になったと感じた場合は、リポジトリのクローンを作成し、ローカルマシンでコードを試してみることをお勧めします。
「このよくある間違いでそれを打ち負かしています。 ここで発明されていない 」症候群は、ソフトウェア開発の世界では非常に一般的です。一般的に使用されるコードの一部を定期的に書き直すなどの症状や、多くの開発者が苦しんでいるようです。
特定のライブラリの内部とその実装を理解することは、ほとんどの場合、適切で必要です(また、優れた学習プロセスになる可能性もあります)が、同じ低レベルの実装に絶えず取り組むことは、ソフトウェアエンジニアとしての開発にとって有害です。詳細。 Springなどの抽象化とフレームワークが存在するのには理由があります。これは、繰り返しの手作業から正確に分離し、ドメインオブジェクトとビジネスロジックというより高いレベルの詳細に集中できるようにするためです。
したがって、抽象化を受け入れます。次に特定の問題に直面したときは、最初にクイック検索を実行して、その問題を解決するライブラリがすでにSpringに統合されているかどうかを判断します。今日では、適切な既存のソリューションが見つかる可能性があります。便利なライブラリの例として、 プロジェクトロンボク この記事の残りの部分の例の注釈。 Lombokは定型コードジェネレーターとして使用されており、あなたの中の怠惰な開発者は、ライブラリに精通することに問題がないことを願っています。例として、「 標準のJavaBean 」は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のバージョンのプラグインが見つかります ここに 。
gulp-clean-css
内部構造を公開することは、サービスデザインに柔軟性を持たせず、その結果、不適切なコーディング慣行を助長するため、決して良い考えではありません。 「リーク」内部は、特定の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
たとえば、より柔軟なソリューションは、TopTalentEntity
を表す新しいクラスを作成することです。 APIエンドポイントのデータ:
@AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; }
そうすれば、データベースのバックエンドに変更を加えるために、サービスレイヤーで追加の変更を行う必要はありません。 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
を指定します。メソッドへのアノテーション(アノテーションの例はよくある間違い#6にあります)。
デスクトップアプリとWebアプリのどちらで発生するか、Springで発生するかどうかに関係なく、マルチスレッド化は難しい問題になる可能性があります。プログラムの並列実行によって引き起こされる問題は、神経質になりにくいものであり、デバッグが非常に難しい場合があります。実際、問題の性質上、並列実行の問題に対処していることに気付いたら、おそらく次のようになります。デバッガーを完全に放棄し、根本的なエラーの原因が見つかるまで「手作業で」コードを検査する必要があります。残念ながら、そのような問題を解決するためのクッキーカッターソリューションは存在しません。特定のケースに応じて、状況を評価してから、最善と思われる角度から問題を攻撃する必要があります。
もちろん、理想的には、マルチスレッドのバグを完全に回避したいと思うでしょう。繰り返しになりますが、これを行うための万能のアプローチは存在しませんが、マルチスレッドエラーをデバッグおよび防止するためのいくつかの実用的な考慮事項を次に示します。
まず、常に「グローバル状態」の問題を覚えておいてください。マルチスレッドアプリケーションを作成している場合は、グローバルに変更可能なものはすべて綿密に監視し、可能であれば完全に削除する必要があります。グローバル変数を変更可能なままにする必要がある理由がある場合は、慎重に採用してください 同期 アプリケーションのパフォーマンスを追跡して、新しく導入された待機期間が原因でアプリケーションが遅くならないことを確認します。
これはからまっすぐに来ます 関数型プログラミング そして、OOPに適合して、クラスの可変性と状態の変化は避けるべきであると述べています。つまり、これは、前述のsetterメソッドを使用し、すべてのモデルクラスにプライベートfinalフィールドを設定することを意味します。それらの値が変更されるのは、構築中だけです。このようにして、競合の問題が発生せず、オブジェクトのプロパティにアクセスすると常に正しい値が提供されることを確認できます。
アプリケーションが問題を引き起こす可能性のある場所を評価し、すべての重要なデータをプリエンプティブにログに記録します。エラーが発生した場合は、どのリクエストを受信したかを示す情報が得られ、アプリケーションが誤動作した理由をより正確に把握できるようになります。ロギングによって追加のファイルI / Oが発生するため、アプリケーションのパフォーマンスに深刻な影響を与える可能性があるため、悪用しないように注意する必要があります。
独自のスレッドを生成する必要がある場合(たとえば、さまざまなサービスへの非同期リクエストを作成する場合)は、独自のソリューションを作成するのではなく、既存の安全な実装を再利用してください。これは、ほとんどの場合、利用することを意味します ExecutorServices そしてJava8のきちんとした関数型スタイル CompletableFutures スレッド作成用。 Springでは、を介した非同期リクエスト処理も可能です。 DeferredResult クラス。
Windowsは何でプログラムされていますか
以前の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文字の長さです)、データが無効な場合は例外をスローします。
これは、採用することではるかにクリーンに実行できます 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[] 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
のアノテーションが付けられているため、SpringはどのBeanをワイヤリングする必要があるかを認識しています。次のような複合注釈:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
複合アノテーション(詳細については、 Springドキュメント Beanを取得するためにスキャンする必要があるパッケージに関するヒントをSpringに提供するだけです。具体的なケースでは、これは、上部(co.kukurin)パッケージの下の次のものが配線に使用されることを意味します。
@Component
(TopTalentConverter
、MyAnnotationValidator
)@RestController
(TopTalentController
)@Repository
(TopTalentRepository
)@Service
(TopTalentService
)クラス追加の@Configuration
があった場合注釈付きクラスは、Javaベースの構成についてもチェックされます。
サーバー開発でよく発生する問題は、さまざまな構成タイプ(通常は本番構成と開発構成)を区別することです。テストからアプリケーションのデプロイに切り替えるたびにさまざまな構成エントリを手動で置き換える代わりに、より効率的な方法はプロファイルを使用することです。
ローカル開発にインメモリデータベースを使用していて、本番環境にMySQLデータベースがある場合を考えてみます。これは、本質的に、2つのそれぞれにアクセスするために異なる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
おそらく、コードをいじっているときに本番データベースで誤ってアクションを実行したくないので、デフォルトのプロファイルをdevに設定するのが理にかなっています。サーバー上で、-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; } }
MiskoHeveryのGoogleトーク 依存性注入の「理由」を詳しく説明しているので、代わりに実際にどのように使用されるかを見てみましょう。関心の分離に関するセクション(よくある間違い#3)では、サービスクラスとコントローラークラスを作成しました。 TopTalentService
という仮定の下でコントローラーをテストしたいとします。正しく動作します。別の構成クラスを提供することにより、実際のサービス実装の代わりにモックオブジェクトを挿入できます。
Pythonでクラス変数を変更する方法
@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 })
これにより、コンテキスト構成を使用してカスタムBeanを単体テストに挿入できます。
ユニットテストのアイデアは長い間私たちにありましたが、多くの開発者はこれを「忘れる」ようです(特にそうでない場合) 必須 )、または単に後付けとして追加します。テストはコードの正確さを検証するだけでなく、さまざまな状況でアプリケーションがどのように動作するかについてのドキュメントとしても機能するため、これは明らかに望ましくありません。
Webサービスをテストする場合、HTTPを介した通信では通常、SpringのDispatcherServlet
を呼び出す必要があるため、「純粋な」単体テストのみを実行することはめったにありません。実際のHttpServletRequest
が発生するとどうなるかを確認します受け取られます(それを 統合 テスト、検証、シリアル化などの処理)。 安心してください 、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
のモック実装を配線しますにTopTalentController
他のすべてのクラスは、アプリケーションクラスのパッケージをルートとするスキャンパッケージから推測される標準構成を使用して配線されます。 RestAssuredMockMvc
単に軽量環境をセットアップしてGET
を送信するために使用されます/toptal/get
へのリクエスト終点。
Springは、使い始めるのが簡単な強力なフレームワークですが、完全に習得するには、ある程度の献身と時間が必要です。時間をかけてフレームワークに慣れることで、長期的には生産性が確実に向上し、最終的にはよりクリーンなコードを記述して、より優れた開発者になることができます。
さらにリソースをお探しの場合は、 Spring In Action Springの多くのコアトピックをカバーする優れた実践的な本です。