PHPerがSpring Bootに入門してみた

ずっとPHP(主にLaravel)を書いていたが、転職をして今はJavaを書いている。10年以上前にSIerとしてJavaを書いていたが、もうかなり忘れてしまった。Java自体も紆余曲折ありつついろいろ進化している。
「みんなのJava OpenJDKから始まる大変革期! - 技術評論社」を読むとJavaの現状を理解できるのでお勧めです。
Javaを思い出すために、よく利用されていそうなフレームワークを使ってサンプルアプリを作りながら入門しようと思い、Spring Bootに挑戦。引っかかった部分や困ったところなどまとめてみた。
作ったもの
DBのCURD、ログイン/ログアウト、これらの自動テスト、マイグレーション、自動コードフォーマッターがある程度できるくらいのアプリケーションを作る。 基本的な部分がこれで理解できると思ったので。
できたものはこれ(画面はまったくこだわっていない、CSSもない)
gorou-178/java-spring-boot-todo - GitHub
フレームワークおよびライブラリ
JDKやフレームワーク・ライブラリは以下を利用
- OpenJDK: 11
- Spring Boot: 2.6.2
- Gradle: 6.9.2
- Tymelefe: 3.0.14
- Flyway: 8.2.0
- checkstyle: 9.3
JDK 17を使ってみたかったのだが、ライブラリ側の対応ができていないみたいで動かない(適切にバージョン指定したら動くかも)。JDK 11だと問題なかったためいったんJDK 11で作ることにした。
Gradleは7系があるが、これがSpring Boot2.6系と合わずGradle6系に下げた。
最初にやったこと
いろいろな記事を読み漁りながら書いていたが、古かったりそれぞれの記事で書き方が異なったりしていて、どう書くべきかの判断がつかなかった。 そこでたどり着いたのがZennの本。ちょうどよいサイズかつ初学者にわかりやすく書かれていたのでざっくり理解しつつ動かして理解ができた。
まず読んだのが「SpringBootに入門するための助走本(Zenn改訂版) - すがりょー」。本の最初に書かれているが
これから初めてSpringBootを触るSpringFramework未経験者向け に、導入をサポートするような記事
このとおりすんなり導入ができました。感謝。 ログイン/ログアウト機能を実現したかったのでSpring Securityを導入する必要があった。
Zennの本「Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン - あしたば」を読んでSpring Securityをざっくりと理解できてとても良かった。こちらも感謝。
どちらも0円で公開されています(2022年3月現在)。私はとても参考になったのでZennでサポートさせていただきました。
Spring Security
これがとても難しかった。どのクラスがどのように動き認証・認可をしているのかが理解しにくい。 デフォルトの仕組みをそのまま利用する場合、Usersテーブルはusername(文字列)で一意管理されており、IDのようなものがなかった。デフォルトのStringでのリレーションが許せなかったため、usersテーブルにidカラム(数値型)を追加するカスタマイズをした。
カスタマイズするにはUsersテーブルのRepositoryと、UserDetailsServiceの実装。既存のUserDetailsServiceの差し替えが必要。 Unit testもカスタマイズしないとデフォルトのUserDetailserviceが動作してしまうので注意。以下記事が参考になった。
Spring の Controllerで認証情報を参照するメソッドをテストするときの準備 - Qiita
Tymelefe
Spring Bootが推奨していたので選択。しかし個人的な感想としてクセが強く使いにくいと思った。 thネームスペースを使った独自タグやattributeを利用する前提。viewのパーツを準備して差し込む機能もあるが、パーツ定義するためにHTML全体を記述しないといけない。
たとえば base.html
に以下を記述
<!DOCTYPE html>
<html lang="ja" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head th:fragment="meta_header(title)">
<meta charset="UTF-8">
<title th:text="${title}+' | sample app'">Default | sample app</title>
</head>
<body>
<header th:fragment="header">
<h1><a th:href="@{'/'}">sample app</a></h1>
</header>
</body>
</html>
th:fragment
でパーツの名称を決める。引数を定義することで、各ページで値を指定して差し替えたりもできる(上の例だと${title}の部分)。
home.html
でbase.htmlのパーツを利用するにはth:replace
を利用する。
<!DOCTYPE html>
<html lang="ja" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head th:replace="base :: meta_header('Home')">
</head>
<body>
<div th:replace="base :: header"></div>
<h2>Home</h2>
...
th:replace="<html名> :: <fragment名>"
と記述すると記述したタグがfragmentのタグに差し替わる(上の例だとth:replaceを記述したdivタグがth:fragmentを記述したheaderタグに差し替わる。divタグは出力されない)。
似たような構文としてth:insert
やth:include
がある。微妙に動作が異なるので公式ドキュメントで確認を。
わかりにくいポイント
以下いろいろ調べてもすぐに分からなかった部分
- ControllerでStringを返す(viewファイル名を返す)場合、直接viewファイル名を記載する。ディレクトリに分ける書き方がすぐに分からなかったが、“path/to/view.html"と書けば分けられた
th:object
を指定した場合、変数展開がth:field
が${xxx}
ではなく*{xxx}
になるspring-boot-starter-security
ライブラリを利用している場合、formタグにth:action
を指定すると自動的にcsrfタグを追加してくれるが、知っていないとわかりにくい
Checkstyle
今回はCheckstyleを利用した。
Gradleの場合だと、gradle check
コマンドでスタイルチェックができる。ルールはxmlファイルを設置することで定義でき細かく変更も可能。
Checkstyle公式ページに「Google’s style」と「Sun’s style」というルール集が紹介されている。それぞれxmlがあるので、これをconfig/checkstyle/checkstyle.xml
として配置することで適用できるので便利。
ただ、「Google’s style」を利用したところ大量に警告が出てきてちょっと圧倒された(細かく見てないが大体はJavaDocの警告だと思う)。最初はもう少しゆるくやってみたかったので、Spring Bootのcheckstyleを利用させてもらった。程よい警告具合で変な書き方にはならない程度のチェックはできていると思う。
Spring Boot checkstyle.xml - GitHub
このあたりは今後もう少し検討したい。
Migration
最初Flywayを利用する前提で、resources/db/migrations
配下にマイグレーションファイルを設置した。h2インメモリDBを利用するよう方針を変えマイグレーションコマンドがあまり意味を持たなくなった(アプリケーションを止めるとすべて消える設定のため)。
Flywayの資産を消そうと考えたが、SQLの設定を調べてみたところそのまま活用できたのでそのまま利用している。
application.ymlファイルのspring.sql.init.schema-locations
を指定することでスキーマSQLの実行が行え、spring.sql.init.data-locations
で初期データの設定ができる。
spring.sql.init.mode=always
の設定をしているので常に初期化されるようにした。productionなどでは利用しないように注意が必要。
Unit test用のDBは、開発用とは別にしたくて設定を変えている。classpathのrootが異なるため、まったく同じマイグレーションファイルをtestフォルダ配下にも現状コピーしている。ここは同じものを利用したいが解決できていない。
セッション管理
Spring Securityがある程度自動でやってくれるのだが、セッションデータの取得が依存してしまっているかつ冗長。ミドルウェアのように前処理で実施したい。もう少しうまいことできると思う。UserDetailsServiceを差し替えている場合、以下のようにPrincipalを強制的にキャストしてログイン情報を取得している(TodoUserがUserDetailsServiceの実装クラス)。
TodoUser user = (TodoUser) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
この書き方はあまり良くないと思っているので、もっとよい書き方に変えたい(もしくはよい感じに隠蔽したい)。
Unit test
アノテーションを記載すればさくっとテストがかけるのでよいが、DBまわりの管理が少し工夫がいる。普通にDB周りのテストを書くとデータが残ってしまい、前後のテストに依存するテストになってしまう。
サンプルは結構良くないほうだと思っている。あちこちのテストで相互に依存している書き方になってしまっているため、もう少しシンプルに書く方法を習得したい。 特にSpring Securityのテストをいい感じにやりたい。
EntityManagerという、LaravelだとModel Factoryみたいなものがあるのだけど、うまく利用できなかった(エラーが解決できず妥協した)。 そのため、RepositoryのテストなのにRepository自身を利用してデータを準備するというテストになってしまった。unit testは改善したいところがたくさんある。
まとめ
試行錯誤して時間がかかりわからなくて妥協したポイントがたくさん。ただ、Zennの本にはとても感謝。初学者にとって、とても良かったのでオススメです。 Spring Securityは初学者泣かせだと思うため、あまりのめり込み過ぎないように注意(どこまでやるか決めておくとよいと思う)。しっかりやるのであれば、適切な書籍を読んだほうが絶対よい。他のコードを見てよい書き方を習得していきたい。
PHPerとしてSpring Boot入門して一番思ったのが、Laravelはとても作りやすかったと改めて思った(初期状態とカスタマイズのしやすさのバランスが良かったと思う)。
あとSpring Bootをやってみて衝撃だったのが、interfaceを定義するだけで実装クラスがビルドで生成される(CrudRepositoryとか)のは驚いた(余談)。
参考URLなど
- Spring Boot
- SpringBootに入門するための助走本(Zenn改訂版) | すがりょー - Zenn
- Javaの基礎を学び終えたアナタに贈る, SpringBoot/SpringSecurityによる掲示板開発ハンズオン | あしたば - Zenn
- Spring Security と Spring Bootで最小機能のデモアプリケーションを作成する - Qiita
- Spring Security with Spring Boot 2.0で簡単なRest APIを実装する - Qiita
- thymeleafの用語から用途がすぐにつかめなかったので、最初にチートシートから入り、tutorialsで細かく見るのがよいと思う
- GradleでCheckStyleを利用する - Zenn
- ログイン後に「ログインしました」みたいな1回だけメッセージを表示させたいときは以下を参照