じゃんけんアドベントカレンダー の 17 日目です。
前回、プレゼンテーション層だけ書き換えて Web API を実装しました。
今回はついに、フレームワークを導入しようと思います。
使うフレームワーク
現在、Java の Web フレームワークとしては
- Spring Framework (Spring Boot)
- Play Framework
あたりが採用されることが特に多いのではないでしょうか。
その他、軽量フレームワークの Spark Framework や、最近だと Quarkus なども有名だと思います。
今回は Spring Framework を導入しようと思います。
Spring の導入
依存関係のセットアップ
Spring を導入するのであれば、プロジェクトの作成には Spring Initializr を使うことをオススメします。
今回導入する依存関係は以下の 5 つになります。
- Spring Web
- Validation
- Spring Boot DevTools
- Lombok (すでに導入済み)
- MySQL Driver (すでに導入済み)
じゃんけんアプリケーションのプロジェクトはすでに存在しているので、Spring Initializr で上記の依存関係を入れたプロジェクトを作成し、生成された設定ファイル等を参照しながら変更していきました。
具体的には この時点の build.gradle 等を参照ください。
今回の変更点
上記の依存関係をセットアップした上で、今回は大きく以下の 3 つの変更を入れました。
- Controller で Spring MVC を使うよう変更
- 依存の解決を ServiceLocator から DI に変更
- Bean Validation によるバリデーションを導入
順にどのように変わったかを解説していきます。
Controller で Spring MVC を使うよう変更
まず、Controller の変更についてです。
もともと JankenAPIController は以下のようになっていました。
@WebServlet("/api/v1/jankens")
public class JankenAPIController extends HttpServlet {
private JankenApplicationService service = ServiceLocator.resolve(JankenApplicationService.class);
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
val requestBody = APIControllerUtils.getRequestBody(request, JankenPostRequestBody.class);
val maybeWinner = service.play(
requestBody.getPlayer1Id(),
requestBody.player1Hand(),
requestBody.getPlayer2Id(),
requestBody.player2Hand());
val responseBody = JankenPostResponseBody.of(maybeWinner);
APIControllerUtils.setResponseBody(response, responseBody);
}
}
これが以下のようになりました。
@RestController
@RequestMapping("/api/v1/jankens")
@AllArgsConstructor
public class JankenAPIController {
private JankenApplicationService service;
@PostMapping
public JankenPostResponseBody post(@RequestBody @Validated JankenPostRequestBody requestBody) {
val maybeWinner = service.play(
requestBody.getPlayer1Id(),
requestBody.player1Hand(),
requestBody.getPlayer2Id(),
requestBody.player2Hand());
return JankenPostResponseBody.of(maybeWinner);
}
}
自作したユーティリティクラス (APIControllerUtils) を使ってリクエストボディを読み込み、レスポンスボディを設定していた箇所が、フレームワークの機能で簡単に扱えるようになっています。
そのほかに、
- 依存の解決を ServiceLocator から DI に変更
- Bean Validation によるバリデーションを導入
といった変更も入っています。
続いてこれら 2 つの変更について説明していきます。
依存の解決を ServiceLocator から DI に変更
DI の導入により Controller、ApplicationService、Repository などコードの各所が変わっていますが、分かりやすい JankenApplicationService を見てみようと思います。
@Service
@AllArgsConstructor
public class JankenApplicationService {
private TransactionManager tm;
private JankenRepository jankenRepository;
private PlayerRepository playerRepository;
:
JankenApplicationService には、@AllArgsConstructor というアノテーションにより、TransactionManager、JankenRepository、PlayerRepository を引数とするコンストラクタが生成されています。
これで、JankenApplicationService を使う際に必要なクラスが、コンストラクタ経由で外部から注入できるようになりました。
このように、あるクラスに必要なクラスを外部から入れることを DI と言います。
JankenCLIApplication のコードでは、もともと ServiceLocator を使って依存解決の設定をしていました。
public class JankenCLIApplication {
public static void main(String[] args) {
// 依存解決の設定
ServiceLocator.register(TransactionManager.class, JDBCTransactionManager.class);
ServiceLocator.register(JankenCLIController.class, JankenCLIController.class);
ServiceLocator.register(PlayerApplicationService.class, PlayerApplicationService.class);
ServiceLocator.register(JankenApplicationService.class, JankenApplicationService.class);
ServiceLocator.register(PlayerRepository.class, PlayerMySQLRepository.class);
ServiceLocator.register(JankenRepository.class, JankenMySQLRepository.class);
ServiceLocator.register(PlayerDao.class, PlayerMySQLDao.class);
ServiceLocator.register(JankenDao.class, JankenMySQLDao.class);
ServiceLocator.register(JankenDetailDao.class, JankenDetailMySQLDao.class);
// 実行
ServiceLocator.resolve(JankenCLIController.class).play();
}
}
これをコンストラクタによる DI に置き換えると、以下のようになります。
public class JankenCLIApplication {
public static void main(String[] args) {
// 依存解決の設定
val tm = new JDBCTransactionManager();
val playerDao = new PlayerMySQLDao();
val jankenDao = new JankenMySQLDao();
val jankenDetailDao = new JankenDetailMySQLDao();
val playerRepository = new PlayerMySQLRepository(playerDao);
val jankenRepository = new JankenMySQLRepository(jankenDao, jankenDetailDao);
val playerApplicationService = new PlayerApplicationService(tm, playerRepository);
val jankenApplicationService = new JankenApplicationService(tm, jankenRepository, playerRepository);
val jankenCliController = new JankenCLIController(playerApplicationService, jankenApplicationService);
// 実行
jankenCliController.play();
}
}
このくらいの規模であればなんとか書けますが、Controller・ApplicationService・Repository といったクラスが増えていくと、上記のコードを書くのは非常に大変です。
そこで Spring などの持っている DI コンテナの機能を使うわけです。
Spring の DI では、依存性を注入する対象クラスのコンストラクタが 1 つなら、そこに自動的に依存関係を入れてくれます。
なので、JankenApplicationService のように @Service などのアノテーションと @AllArgsConstructor アノテーションをつければ、DI の設定は完了です。
JankenApplicationService の全体像は以下のようになりましたが、ServiceLocator を使っていたころとの変更点はアノテーションとフィールドだけです。
@Service
@AllArgsConstructor
public class JankenApplicationService {
private TransactionManager tm;
private JankenRepository jankenRepository;
private PlayerRepository playerRepository;
/**
* じゃんけんを実行し、結果を保存して、勝者を返します。
*/
public Optional<Player> play(String player1Id, Hand player1Hand,
String player2Id, Hand player2Hand) {
return tm.transactional(tx -> {
val janken = Janken.play(player1Id, player1Hand, player2Id, player2Hand);
jankenRepository.save(tx, janken);
return janken.winnerPlayerId()
.map(playerId -> playerRepository.findPlayerById(tx, playerId));
});
}
}
なお、上記のコードで使用している TransactionManager は自前で実装したものであり、Spring が持つ同名の TransactionManager ではありません。
トランザクション管理は本来 Spring の機能で実施すべきですが、そこは次回 OR マッパを導入するのと同時に変更しようと思います。
Bean Validation によるバリデーションを導入
最後に、バリデーションの導入についてです。
JankenAPIController クラスでは、post メソッドの引数 requestBody に、@Validated アノテーションをつけています。
@RestController
@RequestMapping("/api/v1/jankens")
@AllArgsConstructor
public class JankenAPIController {
private JankenApplicationService service;
@PostMapping
public JankenPostResponseBody post(@RequestBody @Validated JankenPostRequestBody requestBody) {
:
これでリクエストを受け取った際にバリデーションが実行されます。
バリデーションの内容は JankenPostRequestBody クラス内にアノテーションで定義しています。
:
class JankenPostRequestBody {
@NotBlank
private String player1Id;
@NotNull
private Integer player1Hand;
@NotBlank
private String player2Id;
@NotNull
private Integer player2Hand;
:
動作確認
これで実装が完了したので、動作確認していきます。
まず、以下のコマンドで Spring Boot を起動します。
./gradlew bootRun
curl でじゃんけんを実行すると、勝者が返ってきます。
$ curl -X POST -H 'Content-Type: application/json' -d '{"player1Id": "1", "player1Hand": 1, "player2Id": "2", "player2Hand": 0}' localhost:8080/api/v1/jankens
{"winnerPlayerName":"Alice"}
バリデーションが実装できていることも確認します。
$ curl -X POST -H 'Content-Type: application/json' -d '{"player1Id": " ", "player1Hand": 1, "player2Id": "2", "player2Hand": 0}' localhost:8080/api/v1/jankens
{"timestamp":"2020-12-16T12:01:41.278+00:00","status":400,"error":"Bad Request","trace":"...
player1Id が半角スペースだけの文字列だと許容できないため、エラーとなりました。
※ 本来は使っているフレームワーク等を特定されないよう、エラーメッセージをデフォルトのまま返すことはしません
まとめ
今回 Spring Framework を導入しましたが、変更点があったのは主にプレゼンテーション層でした。
OR マッパを次回導入予定なので、その際はインフラストラクチャ層が変更になります。
一方、アプリケーション層については、Service アノテーションをつけたり ServiceLocator を DI に置き換えただけで、ほとんど変更はありませんでした。
ドメイン層については一切変更していません。
このように、多くの場合プレゼンテーション層はフレームワークと密結合なため、フレームワークの変更の影響を大きく受けます。
また、インフラストラクチャ層も技術詳細の実現が役割であるため、OR マッパなどの変更の影響を大きく受けます。
一方で、アプリケーション層やドメイン層はどんな技術を使っているかとは関係ないので、技術選択が変わったとしても影響を受けないように分離することが可能です。
現時点のコード
現時点のディレクトリ構成は以下のようになっています。
$ tree app/src/main/java/
app/src/main/java/
└── com
└── example
└── janken
├── JankenCLIApplication.java
├── JankenWebApplication.java
├── application
│ └── service
│ ├── JankenApplicationService.java
│ └── PlayerApplicationService.java
├── domain
│ ├── model
│ │ ├── janken
│ │ │ ├── Hand.java
│ │ │ ├── Janken.java
│ │ │ ├── JankenDetail.java
│ │ │ ├── JankenRepository.java
│ │ │ └── Result.java
│ │ └── player
│ │ ├── Player.java
│ │ └── PlayerRepository.java
│ └── transaction
│ ├── Transaction.java
│ └── TransactionManager.java
├── infrastructure
│ ├── csvdao
│ │ ├── CsvDaoUtils.java
│ │ ├── JankenCsvDao.java
│ │ ├── JankenDetailCsvDao.java
│ │ └── PlayerCsvDao.java
│ ├── dao
│ │ ├── JankenDao.java
│ │ ├── JankenDetailDao.java
│ │ └── PlayerDao.java
│ ├── jdbctransaction
│ │ ├── InsertMapper.java
│ │ ├── JDBCTransaction.java
│ │ ├── JDBCTransactionManager.java
│ │ ├── RowMapper.java
│ │ ├── SimpleJDBCWrapper.java
│ │ └── SingleRowMapper.java
│ ├── mysqldao
│ │ ├── JankenDetailMySQLDao.java
│ │ ├── JankenMySQLDao.java
│ │ └── PlayerMySQLDao.java
│ └── mysqlrepository
│ ├── JankenMySQLRepository.java
│ └── PlayerMySQLRepository.java
├── presentation
│ ├── api
│ │ ├── health
│ │ │ ├── HealthAPIController.java
│ │ │ └── HealthResponseBody.java
│ │ └── janken
│ │ ├── JankenAPIController.java
│ │ ├── JankenPostRequestBody.java
│ │ └── JankenPostResponseBody.java
│ └── cli
│ ├── controller
│ │ └── JankenCLIController.java
│ └── view
│ └── StandardOutputView.java
└── registry
└── ServiceLocator.java
24 directories, 39 files
コードは GitHub の この時点のコミット を参照ください。
次回のテーマ
今回 Spring Framework を導入しました。
実際には、フレームワークを導入するのであれば、同時に OR マッパについても導入することになると思います。
そこで次回は OR マッパやデータマイグレーションツールを導入しようと思います。
それでは、今回の記事はここまでにします。最後まで読んでくださりありがとうございました。
じゃんけんアドベントカレンダー に興味を持ってくださった方は、是非購読お願いします。