【SpringBootに入門する為の助走本】
■おしらせ
本記事はZennに移行して改訂版を出しました。
Zenn版はこのQiita記事を元に分冊化して、一部古すぎる内容をメンテしつつ、Zenn独自記法にあわせて加筆修正したものになります。
今後の加筆修正や改訂はZenn版のみになります。
Qiita版は消さずにこのまま残しておく つもりですが、基本的に更新はせず、最後に「あとがき」だけ追記に来ると思います。
■おしながき
-
環境構築編
- SpringBootと統合開発環境
- JavaのPath確認
- STSの入手
- STSの起動
- STSの日本語化
-
取り敢えずStarterProjectを使ってみる編
- 新しいプロジェクトを作る
- ウィザード1ページめ
- ウィザード2ページめ
- ウィザード3ページめ(さいご)
- プロジェクトが出来上がります。
-
簡単なWebAPIを作ってみよう編
- 取り敢えずHelloWorldする
- パラメータを指定する
- HttpMethodを指定する
- 戻り値をString以外にしてみる
- 共通のエラーハンドラを用意しておく。
-
逆にWebAPIを呼び出してみよう編
- 取り敢えず適当なAPIをコールします。
- exchangeで色々と細かく指定しながらコールする。
- 4xx系/5xx系で例外を発生させなくする方法。
- PATCH だけちょっと注意。
- タイムアウト設定を弄る方法。
- RestTemplateラッパー三点セット
-
Thymeleafで画面を作ってみよう編
- SpringBootがサポートするテンプレートエンジン
- とにもかくにも画面だしてみよっか。
- 取り敢えず日付時刻を出すだけの画面をつくる。
- 式言語:SpEL - Spring Expression Language
- 画面をイケメンにしてみよう。
-
スケジュール機能を使ってみよう編
- スケジュール機能を有効化してみよう。
- スケジュール機能を使ってみよう。
- スケジュール(定期実行タスク)機能に関する余談
-
応用編:Thymeleafをプレーンテキストで使ってみよう編
- Thymeleafとはそもそもなにか?
- とにかくまずはプレーンテキストで使ってみよう!
- TemplateEngineをBean登録して使おう。(その1)
- TemplateEngineをBean登録して使おう。(その2)
- TemplateをDBに文字列で持ちたいです!
- ちょっとしたTips集
※ 肥大化してきたら整理して大見出し毎に別ページに分冊化してリンク張る形に変更・・・するつもりだったけど、どうしようか、どうしたほうがいいかな?
取り敢えず自前でおしながきにリンク貼ってセルフ見出しマップにしました。
■まえがき
興味のない人は 「おしながき」 に書いた見出しまで読み飛ばしちゃっておk!!
きっかけ:
新しいJavaの開発案件でフレームワーク選定してて
(*'▽') SpringBootやろうぜ!!
って事になり、非機能要件について色々と調査・勉強してまして。
調べてプロジェクトで使ってみた感じ
(*'▽') こいつぁ便利で面白いぞ!!
って感じで盛り上がりました。
で、折角なので*(プロジェクトでのドキュメンテーションも兼ねて)*公益性のためにQiitaに纏めて公開しよう、という話になった次第。
これを書いている人について:
- Javaの経験は3~4年程度。
(実務経験Java6~7、趣味でJava8を使っている。もう9とか10とか出ていて割と焦っているだったけど最近普通にJava11とか使ってんねぇ ) - 以前はJavaEE開発をやっていた。
(JSF:primefaces, JPA:hibernate を一応触った事がある) - もともとはC#屋さんで、WebよりWindowsがメインだった人。
- 本当はJavaよりKotlinに興味ニキだけど、今回は開発の都合でJava使うマン。
- SpringFramewrokの使用経験はなく、今回初めてSpringBootを使う。
方向性:
上記と似たような経歴の人や、Spring触った事ないけどこれからSpringBootを始めようとしてる人の参考になればコレ幸い。
入門系のページは既に沢山あって情報が充実してるので、それらへの 「助走本」 を目指す感じ。
以前やってたJavaEE開発(JSF/JPA)に絡めて書いていこうと思うので、そっち方面の経験者には親和性が高くなる、、、かもしれません。
全然絡められてないだるるぉ!いい加減にしろ!
■前提(各種バージョン)
ちなみにこの記事でどこまで書くか決めてないけど、今やってる開発は SpringBoot + Thymeleaf + PostgreSQL
という構成でやってます。
ただこの記事では、DBまわりの話はほぼ出てきません。
(個人的には、SpringDataJPA使うよりはJdbcTemplate使った方が良いかなと思ってる派)
> 初稿時点での環境情報
【※※ 注意事項 ※※】
- 初稿からだいぶ日が経っており、既にSTS3系は古くなってます。現状(2019年時点)ではSTS4系を使った方が良いです。
- また、記事内容とはあんま関係ないですけど、最近自宅PCをRyzenの64bitマシンで組み直したので、自宅開発環境も初稿から大分変わっています。
- 初項時点での環境情報:
使用する各バージョンは以下の通り。
※自宅環境と開発環境でちょっと違うけど、記事の内容的には概ね問題ないと思う。
注意しないといけないのは、JavaとSTSでbitバージョン(x64/x86)を合わせる必要があるという事くらい。
環境 | バージョン |
---|---|
開発言語 | Java8以上 |
開発環境 | STS:3.9.4 RELEASE e4.7.3a ※ |
SpringBoot | 2.0.2 |
ちなみに、個々の細かい説明 (いきなり出て来たSTSって何やぁ? とか) は後述します。
- 現在の環境情報:
ということで、SpringBoot自体の説明にはあんま関係ないですけど、現在の環境を改めて記載しときます。
環境 | バージョン |
---|---|
開発言語 | Java11 (open-jdk 11.0.1) |
開発環境 | STS:4.0.2 RELEASE |
SpringBoot | 2.1.1 |
- 更にバージョンアップ:
先日見に行ったら STS4
がちょっとバージョン上がってて、現時点の最新が 4.4.0
になってました。
何だそんな事か、なんですが、毎回毎回ウザかった pom.xml の一行目に出て来る謎のエラー
が解消されてました。
やったね!!
- 更に更にバージョンアップ:
STS4
が更に更新されており、落とせるのが zip
から jar(自己展開形式)
になってました。
環境構築編のところに詳細追記してあります。
- 2019年12月時点の環境:
という事で、更に日が経って環境も変わり、更にエディタに VSCode
を使用するようになりましたので、改めて現状の最新バージョンを載せておきます。
環境 | バージョン |
---|---|
開発言語 | Java11 (open-jdk 11.0.1) |
開発環境 | STS:4.4.1 RELEASE |
SpringBoot | 2.2.1 |
エディタ | VSCode / SpringBoot Extension Pack |
ということで VSCode - VisualStudioCode を最近使っています。
拡張機能の SpringBoot Extension Pack を入れる事でSpringBootアプリケーションの開発がサポートされます。
VSCodeの マルチカーソル機能 が使えたり、もともと C#出身でVSっ子 のぼくには凄く使い易いエディタですし、起動もめちゃくちゃ軽い し、 編集差分の出し方やgitとの連携・競合発生時のマージ作業 などなど、、、メチャクチャ便利でオススメです。
※ と、大絶賛はしてみたものの、この記事はこのまま「助走本」として、初歩的なSTS環境をベースに書いていこうと思います。
【環境構築編】
SpringBootでの開発では STSというもの を使うようです。
ここではとりあえずその STSというもの を入手して起動する所までをやってみます。
■SpringBootと統合開発環境
STS(Spring tool suite) には、IDEとしての STS.exe
と、プラグインとしての STS plugin
の2種類存在します。
STS.exe
-
Eclipse
+STS plugin
-
IntelliJ
+STS plugin
- (番外編)
VSCode
+SpringBoot Extension Pack
まぁ今回は手っ取り早いので STS.exe
を使います。
ちなみにこの STS.exe
さんですが、何の事はないただの Eclipseベースの拡張IDE です。
要するにSTSを使うということはEclipseにSTSプラグインを適用するのとだいたい同じ事です。
IntelliJ大先生にもプラグインがあったはずなので、IntelliJっ子はソレでいいと思います。
確かCommunityでも使えたと思いますが、あまりガッツリ使わなかったのでちょっと詳しく覚えてません。
VSCode + SpringBoot Extension Pack
は ぼくが最近お気に入りの環境 です。
これは厳密な話をするとIDEじゃなくてエディタなんですが、拡張機能を入れる事でIDEとして必要な機能は揃っちゃうので、実質IDEですねこりゃ。
余談
ここではあくまで入門編への助走を手助けする意味で「助走本」と題しており、いわゆる「初学者」を対象読者と考えています。 ので、基本的には標準(?)のSTS系を前提とします。 どうしてもEclipse系のIDEが嫌いだとか、敢えてIntelliJやVSCodeを使おうという人は既に「初学者」ではないので、よしなに読み替えて下さい。まぁ、基本的なアノテーションの使い方とか、SpringBoot(SpringFramework)自体の使い方としてIDEの違いが問題になることはまず無いと思うので、そこは好きなものを選んで大丈夫だと思います。
ということで、本稿では基本的にSTSベースで進めていきます。
■JavaのPath確認
STSを使う前にJavaのPathが通っているか確認します。
普通にコマンドプロンプト cmd.exe
を立ち上げて、以下のコマンドを叩いてパス通ってるか確認しましょう。
C:\>echo %JAVA_HOME%
C:\openjdk\jdk-11.0.1
C:\>java -version
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)
出て来なかったらパスがおかしいので、システムの詳細設定から環境変数を調べてパスが通っているか確認してください。
そう言えば、本題と関係ないですけど 環境変数の Path
って、Windows10から (もしかして知らなかっただけでWindows8から?) メチャクチャ編集しやすくなりましたよね。
■STSの入手
こちらの 公式ダウンロードページ から入手し、zipを好きな所に展開するだけでOKです。
但し、このページの DOWNLOAD STS
から落とすと x86(32bit)版 が落ちて来たので、x64(64bit)版が欲しい人は See All Versions
リンク先から入手して下さい。
暫く記事の更新が滞ってるうちにバージョンが上がっちょりました。
今は SpringTool4 (現在はsprint-tool-suite 4.1.0) が出ていて、上記のようにダウンロードページもリニューアルされてます。
VisualStudioCode用のプラグインも出てました、素晴らしい!
前のSTS3系が欲しい人は、ページ下部のリンクから入手可能なようですが、
STS3系は今年、2019年の中頃にEOLになるよって書いてあるので、素直にSTS4系にした方が良さそうです。
(なので、敢えてSTS3系のリンクや画像は貼らないでおきます)
> zip が jar になってました。
ちょっと最新のSTSを入れ直そうと思って Spring.io tools に取りに行ったら、以前は zip
だったのが jar
になってました。
spring-tool-suite-4-4.4.1.RELEASE-e4.13.0-win32.win32.x86_64.self-extracting.jar
self-extracting.jar
ということで、なんか自己展開形式? のjar? なんですかね?
Windows環境だと長過ぎるパスを扱えないのでzip形式だと上手く展開出来ないらしく、javaコマンドで実行することでいつものアレが手に入るようになったようです。
java -jar spring-tool-suite-4-4.4.1.RELEASE-e4.13.0-win32.win32.x86_64.self-extracting.jar
■STSの起動
zipを落として来たら、好きなフォルダに展開します。
書いてる内容が大分古くなりました。
上述の通り、zipじゃなく自己展開形式のjarになっています。
展開したディレクトリの中(僕の場合は C:\springboot\sts-bundle\sts-3.9.4.RELEASE
)にある STS.exe
をダブルクリック。
この時、Javaへのパスが通っていない場合は怒られるので、前述の通りきちんとパスの確認をしましょう。
すると、こんな EclipseみたいなIDE (ベースがEclipseなので当たり前) が立ち上がります。
■STSの日本語化
STS.exe
はデフォルトでは日本語化されてないので、別途 Pleiades plugin
を入れないと英語表記になります。
英語表記だと困るよ、っていう方は Pleiades からプラグインを入手して下さい。
> 日本語化の参考資料
> 【余談】Pleiades適用が物凄く簡単になってた件
ぼくは普段(めんどくさいので)日本語化せずに英語のまま使ってるんですが、ちょっと手順確認の為にと思って最新のPleiades先生を入手して、適用してみようと思ったんです。
あれ?
Eclipseの日本語化ってこんなに簡単だっけ?
なんか setup-pleiades.exe で STS.exe 指定したらすぐおわった。
以前はなんかjarとか手で配置しに行ったりして、結構ダルい作業だった気がするんですけど。
いやぁ、、、すごいですね、Pleiades先生。
物凄く便利になってました!!
【取り敢えずStarterProjectを使ってみる編】
■新しいプロジェクトを作る
普通にEclipse感覚で、SpringBootの新規プロジェクトを作成してみます。
[File>New>Spring Starter Project]
から選択。
若しくは [File>New>Other]
で、メニューから [SpringBoot>Spring Starter Project]
を選択。
以下、ウィザードに従っていきます。
■ウィザード1ページめ
さて、いきなり入力項目の多い画面が出て来て嫌になりましたね?
ざっくり各項目を説明しますが、結論から言うと「取り合えずデフォルトのまま突き進んでおk」ですので、かったるい説明が嫌な人は次の見出しまでカッ飛んで下さい。
> ServiceURL
これがSpringBootの正体と言っても過言ではない(多分)、SPRING INITIALIZR大先生です。
取り敢えずデフォルトの https://start.spring.io
のままで良いです。
SPRING INITIALIZR 大先生については後程詳しく書きます。(今は深い事気にせずゴー)
※ ちなみに、 spring initializer
じゃなくて initializr(eがない)
みたいです、なんで?
> Name
プロジェクト名を入力します。
好きな名前を付けて下さい。
今回は面倒くさいのでデフォルトの demo
のまま行きます。
> Type:Maven
ビルドツールを選べます。
デフォルトMavenですが、Gradleが好きって人は変更すれば良いよ。
> Packaging:Jar
実行可能形式JARか、普通にデプロイするWARか選べます。
これも詳しくは後述しますが、SpringBootさんは内蔵Tomcatを持っているので、実行可能形式JARにして単独で立ち上げる事が出来るスグレモノなのです。
> JavaVersion
好きなJavaバージョンを選んで下さい。
※面倒くさいのでデフォルトで突き進んでたら 8 選んでたけど気にしない。皆は素直に 9 か 10 選ぶと良いよ。
> Language
Javaの他にKotlinも選べるよ!!
ことりんかわいいよ、ことりん。
※すいません、言ってみたかっただけです。
> Group/Artifact/Version/Description/Package
適当でおk!!
ぽむぽむ(pom.xml
)に書き込まれるから変えたくなったら変えればいいよ。
■ウィザード2ページめ
プロジェクト依存関係を選択します。
プロジェクト依存関係って何やねん?
ぼくも良く解ってませんが、要するにここで 「使いたいSpringフレームワークを選べ」 という事みたいです。
ここで選んだやつが pom.xml
に書き込まれて、後ほど Maven大先生が必要なjar一式を落としてくれる という手筈。
要するにアレですねVisualStudioのxxprojとかで参照設定追加してdll追加するような作業ですね。
(勿論、後からpom.xmlに手動で追記して、新しい依存関係ライブラリを追加して落として来るとかでもOK)
で、取り敢えず今回は 「簡単なWebAPIを作ってみよう編」 と 「Thymeleafで画面を作ってみよう編」 で使用するので、以下の依存関係を選択します。
※ STS(というかSpringBoot自体)のバージョンが上がって、依存関係のリスト内容もちょっと変わってますね。よしなに読み替えて下さい。というかこの各依存関係の詳細が載ってる一覧とか欲しい、、、欲しくない?
- Template Engines
- Thymeleaf
- Web
- Web
Thymeleaf というのは今回使用するテンプレートエンジンです。
JavaEE開発で使った JSF(primefaces) みたいなもんですね。
まぁあれだ、(jarが) 足りなくなったら (pomに) 足すだけ やから。
あんま深く考えずにサクッと進みましょう、勢い大事、勢い。
今の所DBに接続する所までは考えてませんが、必要になったら JDBC
とか JPA
とか入れましょう。
■ウィザード3ページめ(さいご)
設定確認。
なんか良く解んない画面が出て来ましたね?
https://start.spring.io/starter.zip
?name=demo
&groupId=com.example
&artifactId=demo
&version=0.0.1-SNAPSHOT
&description=Demo+project+for+Spring+Boot
&packageName=com.example.demo
&type=maven-project
&packaging=jar
&javaVersion=1.8
&language=java
&bootVersion=2.0.2.RELEASE
&dependencies=thymeleaf
&dependencies=web
どうやらこれは Spring Initializr
と呼ばれるもののようです。
※ ところでどうでも良いですけど、Initializer じゃなくて Initializr なんですね
詳しい説明は後でやります。
取り敢えず Finish
を押してウィザードを完了しましょう。
■プロジェクトが出来上がります。
暫し待てば demo
プロジェクトが出来上がります。
プロジェクトエクスプローラを開いてみるとこんな感じ。
pom.xml
を開いてみると、依存関係選択のページで選択した奴が書かれてますね。
プロジェクトツリーの Maven Dependencies
を開くと、Maven大先生が依存関係解決で落としてくれたJarがワンサカ詰まっております。
【簡単なWebAPIを作ってみよう編】
■取り敢えずHelloWorldする
まぁまずは適当にコード書いて動かしてみましょう。
> controller用のパッケージを作ります。
最初は demo パッケージしか無いので com.exapmle.demo.controller
を作ります。
> WebAPI用のControllerを作ります。
適当に好きな名前でクラスを作って @RestController
アノテーションを付けます。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WebApiController {
// 内容は後で実装するよ。
}
> RequestMappingを設定します。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api")
public class WebApiController {
@RequestMapping("hello")
private String hello() {
return "SpringBoot!";
}
}
で、これにアクセスするために application.properties
(最初にデフォルトで作られているので、リソース検索してください)に好きなポート番号を設定。
server.port=8080
> 動かしてアクセスしてみる。
先の実装でこんな感じで設定しました。
- クラスに
@RequestMapping
をアノテートして引数に"api"
と設定 - メソッドに
@RequestMapping
をアノテートして引数に"hello"
と設定 - アプリケーション設定に
server.port=8080
と設定
なので、このAPIのURLは http://localhost:8080/api/hello
となります。
プロジェクトのコンテキストメニューから Run AS > Spring Boot Application
を選択すると内蔵tomcatが立ち上がって、裏で色々動いてくれます。(超らくちん!)
ブラウザで先のURLにアクセスすると、文字列が返されます。
「取り敢えず動かしてみる」と言うだけなら1クラス実装するだけで良い、というお手軽さに鼻血が出そうです。
とは言え、固定のプレーンテキストを返されても嬉しくはない、この後色々と作り込んでいき、戻りをJSONにしたり、幾つかの方法でパラメータを渡してみたりしましょう。
■パラメータを指定する
> パスパラメータ
@RequestMapping("/test/{param}")
private String testPathVariable( @PathVariable String param ) {
return "受け取ったパラメータ:" + param;
}
このように、@RequestMapping
でマッピングするURLの中に {xxxx}
を仕込んでおくと、@PathVariable
でアノテートしたパラメータにバインドしてくれます。
ちなみに @PathVariable(name)
を省略した場合は、仮引数名と同じ name
を指定したのと同じ事になります。
つまり、上記のコードは以下のように記述する事も可能です。
@RequestMapping("/test/{param}")
private String testPathVariable( @PathVariable("param") String param ) {
@RequestMapping("/test/{hoge}")
private String testPathVariable( @PathVariable("hoge") String param ) {
> リクエストパラメータ
@RequestMapping("/test")
private String testRequestParam( @RequestParam() String param ) {
return "受け取ったパラメータ:" + param;
}
REST API
作るなら基本的に前述の @PathVariable
の方を使用するかと思いますが、念の為、普通に(?)URLのクエリパラメータを使うパターンも載せておきます。
強いて言えば、ダミーのHTMLで適当に<form>
だけ置いて、パラメータを<input:text>
でsubmit掛ける場合とかに使えると思います。
こちらも先の例と同様に、@RequestParam(name)
を省略した場合は、仮引数名と同じ name
を指定したのと同じ事になります。
つまり、上記のコードは以下のように記述する事も可能です。
@RequestMapping("/test")
// /test?param=hoge とした場合、paramには "hoge" がバインドされる。
private String testRequestParam( @RequestParam("param") String param ) {
return "受け取ったパラメータ:" + param;
}
@RequestMapping("/test")
// /test?name=hoge とした場合、paramには "hoge" がバインドされる。
private String testRequestParam( @RequestParam("name") String param ) {
return "受け取ったパラメータ:" + param;
}
余談:
と、ここまで書いてちょっと不安になったのが GET + RequestBody
ってホントに出来ないのか?
いやいやそんな話は無いよなぁ、絶対無いよなぁ、と思ってぐぐってみたら面白い記事が見付かったので、貼っておきます。
> リクエストボディ
通常、リクエストボディを送信する以上、メソッドは POST
になるので、上の2パターンとはちょっと指定が変わります。
(と言っても、上のパターンでも GET
や POST
を明示的に指定出来るのを省略してるだけなので、実際は同じですが)
@RequestMapping(value = "/test", method = RequestMethod.POST)
private String testRequestBody( @RequestBody String body ) {
log.info( body );
return "受け取ったリクエストボディ:" + body;
}
> 全部纏めたコード
以上、取り敢えず最低限おさえておくべき、パラメータの受け取り3パターンはこんな感じ。
今回は String
一個だけという最小構成気味な形だけに限りましたが、まぁ色々出来るんでその辺は色々やってみれば良いと思います。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* REST API のサンプルコントローラ.
*
* <p>
* {@code application.properties#server.port = 9999} と設定していると仮定して、
* {@code "localhost:9999/sample/api/test/*"} にアクセスする。
* </p>
*/
@RestController
@RequestMapping("/sample/api")
public class SampleRestApiController {
private static final Logger log = LoggerFactory.getLogger( SampleRestApiController.class );
@RequestMapping("/test/{param}")
private String testPathVariable( @PathVariable String param ) {
log.info( param );
return "受け取ったパラメータ:" + param;
}
@RequestMapping("/test")
private String testRequestParam( @RequestParam() String param ) {
log.info( param );
return "受け取ったパラメータ:" + param;
}
@RequestMapping(value = "/test", method = RequestMethod.POST)
private String testRequestBody( @RequestBody String body ) {
log.info( body );
return "受け取ったリクエストボディ:" + body;
}
}
■HttpMethodを指定する
/** 登録:CRUDでいう <b>C:CREATE</b> を行うAPI */
@RequestMapping(value="/resource", method=RequestMethod.POST)
private String create(@RequestBody String data) {
return "登録だよ";
}
/** 参照:CRUDでいう <b>R:READ</b> を行うAPI */
@RequestMapping(value="/resource/{id}", method=RequestMethod.GET)
private String read(@PathVariable String id) {
return "参照だよ";
}
/** 削除:CRUDでいう <b>D:DELETE</b> を行うAPI */
@RequestMapping(value="/resource/{id}", method=RequestMethod.DELETE)
private String delete(@PathVariable String id) {
return "削除だよ";
}
/** 更新:CRUDでいう <b>U:UPDATE</b> を行うAPI */
// ※PUTを使うのかPATCHを使うのかと言うのはまた別な話として、、、
@RequestMapping(value="/resource/{id}", method=RequestMethod.PUT)
private String update(@PathVariable String id, @RequestBody String data) {
return "更新だよ";
}
ところで、ソース内のコメントにも記載した通り 更新のAPIに PUT
を使うのか PATCH
を使うのか と言う話ですが。
この話題についてはこの辺が参考になるかと。
- REST入門 基礎知識 - Qiita
- PUT か POST か PATCH か? - Qiita
- リソースの一部更新におけるURL設計
- REST API Tutorial HttpMethods #summary
■戻り値をString以外にしてみる
与太話
別に戻り値 `String` のままでも、中身をJSON形式文字列にしてやればJSONデータを返せるものの、 「本当にJSON形式文字列だけが返却されるのか?」 「どころかHTMLが返って来てたりして?」 「と見せ掛けてエラーメッセージが文字列で返されるかも?」 「HTMLといったな、スマンありゃXMLだった」 「JSONだと思った!?残念、CSV形式文字列でした~!!」 など、不安が尽きませんよね。 この `String` の中身は本当にJSONなのか!? と言う疑念を抱いてわざわざコードパスを舐めるように追いかけるのは苦痛だし、時間の無駄です。結論から言って、Beanを返すだけ です。
SpringBoot先生のRestControllerでは、いわゆるJavaBean(POJO)を素直に返すとそのJSONを返すようにしてくれます。
(裏ではjacksonを使ってstringifyしてるはず)
> JavaBeanを返してよしなにしてもらう。
public static class HogeMogeBean {
private String hoge;
private int moge;
// property と ctor は省略。
}
@RequestMapping("hogemoge")
public HogeMogeBean hogemoge() {
return new HogeMogeBean( "ほげ", 1234 );
}
http://localhost:8080/api/hogemoge
にアクセスして実行するとこんな感じ。
{"hoge":"ほげ","moge":1234}
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: アクセス日時
ちゃんと application/json;charset=UTF-8
も付けてくれてます。
> JSON文字列にちゃんとヘッダ付けてあげる。
逆に、StringでJSON文字列を返してる場合にこのヘッダを付けたい場合はこんな感じ。
@RequestMapping(value = "hogemoge2",
produces = MediaType.APPLICATION_JSON_VALUE)
public String string() throws Exception {
HogeMogeBean bean = new HogeMogeBean("もげ", 297);
String json = new ObjectMapper().writeValueAsString(bean);
return json;
}
アクセスするとこう。
{"hoge":"もげ","moge":297}
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 28
Date: アクセス日時
> めんどくさいのでマップで返しちゃう。
機能設計としての是非は置いといて、さくっと HashMap
を返しても大丈夫。
@RequestMapping("hogemoge3")
public Map<String, Object> map() {
Map<String, Object> map = new HashMap<>();
map.put("hoge", "ぴよ");
map.put("moge", 999);
return map;
}
{"moge":999,"hoge":"ぴよ"}
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: アクセス日時
> ファイルを返す。
org.springframework.core.io.FileSystemResource
てのを使うと楽に実装できるらしい。
@RequestMapping( value = "hogemoge4",
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE )
public Resource file() {
return new FileSystemResource( new File("C:\\test\\hogemoge.png") );
}
試しに画像を置いてアクセスしてみたところ、画像が表示された。
HTTP/1.1 200
Accept-Ranges: bytes
Content-Type: image/png
Content-Length: 1568934
Date: アクセス日時
きちんと Content-Type も image/png
になってる。
試しにテキストファイルに変更してみたらちゃんと text/plain
になってくれた。
これは便利。
下記参考ページにも書いてあるけど、いちいち自分で HttpServletResponse
使ってうにょうにょしなくて良いのが素晴らしい。
というかそのへんを上手いことラップしてるフレームワークなんだから、Http何某の類を直接触るのは極力避けたいですよね。
参考:
■共通のエラーハンドラを用意しておく。
いわゆる 集約例外ハンドラ の実装です。
> コントローラ単一での例外ハンドラ
まずは、単一コントローラに対する例外処理の集約について。
RestController に @ExceptionHandler
でアノテートしたメソッドを用意します。
これで、コントローラで発生した未トラップの例外(いわゆるunhandling-exception)を纏めて処理できます。
C#とかで言う所の AppDomain.UnhandledException
みたいな使い方が出来るアレですね。
@ExceptionHandler
private ResponseEntity<String> onError( Exception ex ) {
log.error( ex.getMessage(), ex );
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
String json = JsonMapper.map()
.put( "message", "API エラー" )
.put( "detail", ex.getMessage() )
.put( "status", status.value() )
.stringify();
return new ResponseEntity<String>( json, status );
}
登場する JsonMapper
っていうのはぼくが com.fasterxml.jackson.databind.ObjectMapper
をラップして作ったユーティリティ(実装内容はコチラ - Gist )です。
ここではエラー用のサンプルBeanを作るのが面倒だったので、単にJSON文字列を作ってるだけです。
実際の開発ではちゃんと ErrorResponse
的なものを作って ResponseEntity<ErrorResponse>
として全体で統一したほうが良いでしょう。
で、コイツの検証用にこんなエンドポイントを追加。
@RequestMapping("test/ex")
public String testException() throws Exception {
throw new RuntimeException( "エラー発生" );
}
で http://localhost:8080/api/test/ex
にアクセスしてみるとこんな感じ。
{"detail":"エラー発生","message":"API エラー","status":500}
Request URL: http://localhost:8080/api/test/ex
Request Method: GET
Status Code: 500
Remote Address: [::1]:8080
Referrer Policy: no-referrer-when-downgrade
> コントローラ横断での例外ハンドラ
@ControllerAdvice/@RestControllerAdvice
を用いることで、複数の Controller/RestController
で @ExceptionHandlerメソッド
などの特殊なメソッドを共有出来ます。
処理のライフサイクル的には、本来こっちを使って集約例外ハンドラを実装するべきのようです。
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler
private ResponseEntity<String> onError( Exception ex ) {
log.error( ex.getMessage(), ex );
// 以下略
こんな感じで、クラスレベルに @ControllerAdvice/@RestControllerAdvice
をアノテートすることで、コントローラを横断して反応する @ExceptionHandlerメソッド
を定義できます。
- 参考情報
> ErrorControllerを作成する
Spring公式の エラー処理 - spring.io にも記載されているが、デフォルトではすべてのエラーは /error
にマップされる。
SpringBootには標準で basic-error-controller : Basic Error Controller
なるコントローラが存在する(ちなみにこれはswagger.uiとかで見ると居るのが解る)が、よしなにカスタムしたい場合は自前で ErrorController実装 をつくりたまえ。
って書いてあるっぽい。
By default, Spring Boot provides an
/error
mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container.
To replace the default behavior completely, you can implement ErrorController
> 別解:共通の基底クラスを作って共有させる
基底クラスで実装共有させるってのは賛否が分かれそうですが、一応実現手段のひとつとして紹介。
@ExceptionHandlerメソッド
を持つ共通基底クラスを作成しておき、コントローラ個別に例外ハンドラを持たせる案。
これはコード例は省略して 実験コードと実験結果を纏めたGist の方を参照して下さい。
以下、余談。
共通実装を 「継承によって共有」 ってのは、本来的には DIコンテナ管理Beanのインジェクションと言う思想に思いっきりぶつかってる ので良くない実装ではあるでしょう。
ただ、この実験コードは例外処理の共有だけじゃなく 「RestControllerでもテンプレートメソッドパターンが使える」事を示している ので、そっちの方向で使いみちがあるかなと思います。
★RestAPI関連の参考ページ★
この記事はなんと言っても 助走本 を目指していますので、
走り出したらこの辺の豊富な素晴らしいドキュメントを読みに行きましょう。
■Spring公式■
まぁまずは公式ガイド。
■Controller関連■
SpringMVCやコントローラ周辺の参考資料としてはこの辺が勉強になりました。
- Spring MVCのコントローラでの戻り値いろいろ - Qiita
- SpringBoot使い方メモ>Spring MVC の簡単な使い方メモ - Qiita
- Spring MVC コントローラの引数 - Qiita
- REST API設計者のための有名APIのURL例 - Qiita
- TERASOLUNA Server Framework for Java
■Security関連■
セキュリティ系に関してはこの辺の記事。
- SpringBootを使うときに最低限やっておきたいセキュリティ対策 - Qiita
- Spring Security with Spring Boot 2.0で簡単なRest APIを実装する - Qiita
■その他Tips系■
■POSTが必要なエンドポイントの動作検証@PowerShell
普通にSpringBootの範囲内でやろうとすると、後述するThymeleafとかで検証用の画面を作ってPOSTするなり、なんか別なプログラム書いてHttpClient使ってPOSTするなり、或いは素直にPostman使うなりになるかと思いますが、PowerShellを使ってコールするというぼく好みの手法が紹介されてました。
一応、Postmanの記事も置いておきます。
【逆にWebAPIを呼び出してみよう編】
外部のAPIをコールするHTTPクライアントとして RestTemplate というものを使います。
※JdbsTemplateといい、RestTemplateといい、SpringさんはHogeHogeTemplateってのが好きみたいですね。
■取り敢えず適当なAPIをコールします。
無料で使えて、且つ面倒な認証が必要ない適当なAPIを探して、試しにGETしてみます。
/**
* @return 東京の天気情報
*
* @see <a href="http://weather.livedoor.com/weather_hacks/webservice">お天気Webサービス - livedoor</a>
*/
@RequestMapping( value="weather/tokyo"
, produces=MediaType.APPLICATION_JSON_VALUE
, method=RequestMethod.GET)
private String call() {
// "http://localhost:8080/api/weather/tokyo" でアクセス。
RestTemplate rest = new RestTemplate();
final String cityCode = "130010"; // 東京のCityCode
final String endpoint = "http://weather.livedoor.com/forecast/webservice/json/v1";
final String url = endpoint + "?city=" + cityCode;
// 直接Beanクラスにマップ出来るけど今回はめんどくさいのでStringで。
ResponseEntity<String> response = rest.getForEntity(url, String.class);
String json = response.getBody();
return decode(json);
}
// いわゆる日本語の2バイト文字がunicodeエスケープされてるので解除。
private static String decode(String string) {
return StringEscapeUtils.unescapeJava(string);
}
実用的にするなら取ってきた情報をそのまま返すんじゃなくて、きちんとBeanにマップした上で必要な情報だけ返すとか、複数API組み合わせていい感じに纏めて返すとか、付加価値的な物があった方が良いけど。
取り敢えずここではAPIを単品で叩いてみるってだけ。
http://localhost:8080/api/weather/tokyo
にアクセスするとこんな感じになりました。
,"description":{"text":" 本州付近は高気圧に覆われていますが、東海道沖から伊豆諸島南部は気圧
の谷となっています。
【関東甲信地方】
関東甲信地方は、晴れまたは薄曇りとなっています。
21日は、高気圧に覆われてはじめ晴れる所もありますが、気圧の谷や湿
った空気の影響で次第に曇るでしょう。夜は、伊豆諸島と沿岸部を中心に、
雨や雷雨となる所がある見込みです。
22日は、前線や気圧の谷の影響により曇りや雨で、伊豆諸島と沿岸部で
は、はじめ雷を伴う所があるでしょう。夜は、冬型の気圧配置となるため、
おおむね晴れますが、長野県北部と関東地方北部の山沿いでは雨や雪の降る
所があり、長野県では雷を伴う所がある見込みです。
関東近海では、21日から22日にかけて、うねりを伴って波が高いでし
ょう。船舶は高波に注意してください。
【東京地方】
21日は、はじめ晴れますが次第に曇りとなり、夜遅くには雨となるでし
ょう。
22日は、曇りで、昼前から昼過ぎにかけて雨となる見込みです。"
, ...
ちなみに、使ったAPIが返してくるJSON文字列内の日本語部分がunicodeエスケープされており、元に戻すのに apache-StringEscapeUtils を使ってるので、pomに追加してimportして下さい。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.6</version>
</dependency>
import org.apache.commons.text.StringEscapeUtils;
■exchangeで色々と細かく指定しながらコールする。
ここまでで紹介した機能で、非常にカンタンなGETやPOSTは出来るようになりました。
が、現実的な難易度の実装で行くと、前述の説明で書いたような 「特定URLに向けてHttpMethodを指定してアクセスするだけ」 というレベルで済む事はまぁまず無いと思います。
実際の開発になると 「認証情報をヘッダに含めてリクエスト飛ばす」 など、他にも色々とやらないといけない事が出て来ますよね。
> RequestEntity/ResponseEntity
リクエストやレスポンスに関して、細かくやりたいことがある場合は、それぞれ org.springframework.http.RequestEntity
と org.springframework.http.ResponseEntity
を使用します。
- spring framewrok
実際にコード見せた方が解り易いと思うので、 ヘッダを指定してJSON文字列を指定エンドポイントにPOSTする簡単な実装例 を示します。
import java.net.URI;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
// 後述する幾つかの差し替え実装は省略。(詳しくはこの後の「RestTemplateラッパー三点セット」のあたりを参照)
private final RestTemplate rest = new RestTemplate();
/**
* POST実装例.
*
* 指定したエンドポイントに対して {@code json} データをPOSTし、結果を返す。
*
* @param url エンドポイント
* @param headers リクエストヘッダ
* @param json 送信するJSON文字列
* @return 正常に通信出来た場合はレスポンスのJSON文字列を、<br>
* 正常に通信出来なかった場合は {@code null} を返す。
*/
public String post(String url, Map<String, String> headers, String json) {
RequestEntity.BodyBuilder builder = RequestEntity.post( uri( url ) );
for ( String name : headers.keySet() ) {
String header = headers.get( name );
builder.header( name, header );
}
RequestEntity<String> request = builder
.contentType( MediaType.APPLICATION_JSON_UTF8 )
.body( json );
ResponseEntity<String> response = this.rest.exchange(
request,
String.class );
return response.getStatusCode().is2xxSuccessful() ? response.getBody() : null;
}
private static final URI uri( String url ) {
try {
return new URI( url );
}
// 検査例外はうざいのでランタイム例外でラップして再スロー。
catch ( Exception ex ) {
throw new RuntimeException( ex );
}
}
RequestEntity
には #post()
や #get()
というHTTPメソッドに対応したbuilderを生成するAPIがあり、何となく使い方は見てすぐ解るかと思います。
上記はPOSTの実装例ですが、GETやDELETEでも大きな違いはありません。
こんな感じで、例えば ヘッダに Authorization Barer
認証トークンを埋めてリクエストする、みたいな事に対応します。
■4xx系/5xx系で例外を発生させなくする方法。
RestTemplate先生は、デフォルトのままだと2xx系以外のHttpStatusコードが返ってきた時に強制的に例外を発生させてきます。
HttpStatus | スローされる例外 |
---|---|
4xx | HttpClientErrorException |
5xx | HttpServerErrorException |
xxx | UnknownHttpStatusCodeException |
基本的にはこれで困る事は少ないと思いますが、システム要件的にもっと詳細情報を拾う必要があったり、例外ルートではなく正常ルートできちんと処理したいというケースも有り得ます。
(実際、今回の開発では外部APIコールに対する詳細情報が必要だったので、例外発生だと不都合でした)
こういった場合は、RestTemplateのデフォルトエラーハンドラを差し替え てやります。
private static class QuietlyHandler extends DefaultResponseErrorHandler {
@Override
public void handleError( ClientHttpResponse response ) throws IOException {
// 何もしない
}
}
protected final RestTemplate rest;
public HttpClient() {
this.rest = new RestTemplate();
this.rest.setErrorHandler( new QuietlyHandler() );
}
デフォルトで使用されている DefaultResponseErrorHandler
を継承して #handleError
をオーバーライドして NOP(なにもしない)処理で上書き しています。
(デフォルト実装ではここで4xx/5xx系の場合に前述の例外をスローする処理が働きます)
あとは、使用する RestTemplate
インスタンスの ErrorHandler
プロパティに自前のエラーハンドラを設定してやるだけ。
これで、4xx/5xx系のステータスコードの場合でも、2xx系と同じ正常ルートでレスポンスを受け取れるようになります。
なお、この場合はステータスコードが 2xx系
なのかそれ以外なのか判定する必要があるので ResponseEntity.StatusCodeプロパティ で取得できる HttpStatus の各判定を参照します。
HttpStatus status = response.getStatusCode();
boolean information = status.is1xxInformational();
boolean success = status.is2xxSuccessful();
boolean redirection = status.is3xxRedirection();
boolean clientError = status.is4xxClientError();
boolean serverError = status.is5xxServerError();
boolean error = status.isError(); // clientError || serverError
参考:
■PATCH だけちょっと注意。
本件は先に参考を載せておきます。
- RestTemplateに追加されたpatchForObjectはデフォ実装だと実質使えません - Qiita
-
RestTemplate PATCH request - StackOverflow
- ※ 雑な和訳 ※
- 「ヘイTom、ボクはPATCHリクエストを投げたいんだがエラーが出るんだ、どうしたらいい?」
- 「やあBob、キミのRestTemplateインスタンスにHttpRequestFactoryを食わせてやれば、きっと幸せになれるよ。」
- 【注意】リンク先にはトムもボブも出てきません。
という事で、大変便利な RestTemplate
先生ですが、せっかく patchForObject や patchForEntity といった いかにもPATCHメソッドが使えそう なAPIがあるのに デフォルトでは使えない っていうね。
で、StackOverflow
でトム(仮)が「ApacheのHttpComponentsClientHttpRequestFactoryを使えば良いよ」と言ってpomを貼ってくれてますが、SpringFramework3.1
からは org.springframework.http.client.HttpComponentsClientHttpRequestFactory
ってのが追加されてて、どうやらコイツが内部的にApacheの某を使っているのでこれを使えば良さそう。
ClientHttpRequestFactory implementation that uses Apache HttpComponents HttpClient to create requests.
雑な和訳
コイツはリクエストを生成するのに「Apache HttpComponents HttpClient」を使用したタイプのClientHttpRequestFactory実装だぜ。
的な事が書いてあります。
詳しく知りたい人はChrome先生に翻訳して貰えばいいと思います。
(最近の翻訳ってメチャクチャ精度上がってるよね)
ということで、前置きが長くなったけどRestTemplateでPATCHを使いたい場合はこんな感じ。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.10</version>
</dependency>
上記の通り、内部的に org.apache.httpcomponents
を使用してるので、この依存関係を追加しとかないとあとで ClassNotFoundException
が出ちゃうよ。
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
RestTemplate rest = new RestTemplate();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
rest.setRequestFactory(requestFactory);
これでも別に問題なく動くけど、下記のタイムアウト設定の方もあわせてやっといたほうが良いですね。
■タイムアウト設定を弄る方法。
タイムアウト設定に関しても、RestTemplate
に設定するのではなく、上記PATCHの時に使った RequestFactory
に対して設定します。
RestTemplate rest = new RestTemplate();
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectTimeout);
requestFactory.setReadTimeout(readTimeout);
rest.setRequestFactory(requestFactory);
PATCHを使う必要がないのであれば、デフォルトの SimpleClientHttpRequestFactory
を使えば追加のjarが不要ですね。
■RestTemplateラッパー三点セット
という事で、それなりの規模の開発をするのであれば、RestTemplateを各所で直接使用するのではなく、ラップした共通部品を作って以下の3点セット対応をした方が無難かと思われます。
- デフォルトエラーハンドラの差し替え
- PATCH対応
- タイムアウト設定
まぁ、その辺は開発指針やプロジェクトの体制にもよりますけど。
★外部API呼び出しの参考ページ★
- 参考情報
- 参考実装
【Thymeleafで画面を作ってみよう編】
■SpringBootがサポートするテンプレートエンジン
- SpringBoot のサポートする テンプレートエンジン
- Groovy
- Thymeleaf ★
- FreeMaker
- Mustache
今回は Thymeleaf(タイムリーフ:タイムの葉っぱ)
を使用します。
- タイムリーフの利点
- 独自タグを使用しない (メタ属性を仕込む) ため、HTMLを汚さない。 ★
- HTMLを汚さないので、そのままブラウザで表示できる。
(サーバ処理なしでも画面表示できる) - そのままブラウザで表示できるので、デザイナと実装の親和性が高い。
(と、いわれている) - HTML用 (view-template) だけでなく、プレーンテキスト用にも使える。 ★
(いわゆる mail-template のような使い方も出来る) - 値をバインドするEL式が比較的わかりやすく、学習コストが低い。
(たぶん。JSFとかやってた人なら「ふーん、なるほどね」って感じで見れると思う。) -
日本語のドキュメント が充実している。
(英語が苦手なフレンズでもあんしん!!)
■とにもかくにも画面だしてみよっか。
取り敢えずバックエンドのモデル(コントローラ)なしで出来る、まっさらなハリボテ画面だけ出してみましょう。
resource/template/index.html
を作成し、内容をこんな感じにします。
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SpringBoot - テスト用画面</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="UTF-8" />
<style type="text/css">
form {
border-style: solid;
border-color: black;
border-width: 1px;
margin: 5px;
padding: 10px;
}
</style>
</head>
<body>
<h1 th:text="'これはThymeleafですか?'">html-見出し1</h1>
はい、SpringBootで推奨されています。
</body>
</html>
兎にも角にも表示させてみましょう。
デプロイ*(っていうか Run as SpringBootApp)*して http://localhost:8080
にアクセスします。
すると、こんな画面が出た筈。
次に、 この resource/template/index.html
を直接ブラウザで 開いてみましょう。
今度は、こんな画面が出た筈。
<h1 th:text="'これはThymeleafですか?'">html-見出し1</h1>
この部分に違いが出ていますね。
直接ブラウザでhtmlファイルを表示した場合、th:text
という属性は標準HTMLに存在しないなので華麗にスルーされ、普通にタグボディに設定されている html-見出し1
という見出しが表示されます。
対して、localhost
で(つまりサーバ側のテンプレートエンジンThymeleafを通して)表示したHTMLレスポンスでは、th:text
属性値が処理されてタグボディに埋め込まれて これはThymeleafですか?
という見出しが表示された、という形です。
と言ってもこれだけやっても別に何も嬉しくない。
サーバ側で動的な値を埋め込んだりしてこそ意味があるので、なんとなーくその辺を追加していきましょう。
■取り敢えず日付時刻を出すだけの画面をつくる。
すげーシンプルに、静的な値ではなく動的な値で、且つインプットを必要としない、最小構成的な物はなにか無いかな。
と思って考えてみた結果 「取り敢えず日付時刻でいいんじゃネ?」 と思ったので 日付時刻 を出すだけの画面 what time is it?
を作ってみます。
> What time is it コントローラ の作成
まず、コントローラのガワだけ作っちゃいましょう。
お好きなパッケージ(ぼくの場合は [ProjectRoot].app.controller.view
に作りました)にWhatTimeIsItControllerクラス(クラス名は何でも良い)を作り、@Controller をアノテートします。
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("view/what-time-is-it")
@Controller
public class WhatTimeIsItController {
// まだじっそうしてないよ。
}
実装内容はまだりません、これから作り込んで行きますが、その前に View(html) の方を用意しちゃいましょう。
※ コントローラの分割レベルとかも議論になるかと思いますが、ここでは(助走本のレベルを超えてるので)華麗にスルーします。
> What time is it 画面HTML の作成
次に、これから作る画面のHTMLファイルを作成しましょう。
source/main/resources
配下に templates
フォルダがあって、現状 index.html
が置いてあるかと思います。
実際の業務開発ではこの辺は良い感じにフォルダ構成とか切っていくと思いますが、面倒なのでここでは直置きしちゃいましょう。
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>sugaryo.spring-boot-tips</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="UTF-8" />
<link th:href="@{/css/common.css}" rel="stylesheet"></link>
<script th:src="@{/js/common.js}"></script>
</head>
<body>
<h1 th:text="'■What time is it?■'"></h1>
<div class="border">
まだなにもないよ。
</div>
</body>
</html>
ハイ、こんな感じ。
それではこれからコントローラで動的な値を生成し、それを埋め込んでViewテンプレートに渡し、Thymeleafの機能を使ってHTMLに埋め込んでブラウザの画面に表示する所までやってみましょう。
このあとやる事はだいたいこんな感じ。
- コントローラに画面のパス(
@RequestMapping
)を設定。 - コントローラ(サーバ処理)で 動的な値 を生成。
(今回は単純に日付時刻の文字列を作る) - SpringMVCのお作法に則って、生成した 動的な値 を設定。
- 設定した 動的な値 を、ThymeleafのViewテンプレートで参照。
(詳細は後述) - ブラウザで画面にアクセスすると、サーバ処理で生成した 動的な値 が表示される、、、 はず!
> What time is it コントローラ の実装
まずコントローラのクラスにこんなメソッドを作ります。
@GetMapping()
public String view( Model model ) {
return "wtii"; // ここで返すのはView名、つまり `wtii.html` の "wtii" を返す。
}
コメントに書いた通り、基本的にViewController( @Controller
でアノテートした画面のコントローラ)は、String(ビュー名)を返すメソッド を実装します。
厳密には ビュー名 + メタ情報 で :forward
を指定したりするんですが、取り敢えずここでは 「遷移したい画面のView名を返すと、SpringBoot先生がよしなに画面遷移してくれるんだな」 とだけ思ってください。
ほんで、これだけだとまだ固定の画面を出すだけで、最初に作った index.html
と大差ありません。
ので、これから上記メソッドを改造してサーバ側の日付時刻文字列を生成して埋め込む所まで作り込みます。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ui.Model;
@GetMapping
public String view( Model model ) { // ①
String now = LocalDateTime.now().format( DateTimeFormatter.ISO_DATE_TIME ); // ②
model.addAttribute( "datetime", now ); // ③
return "wtii";
}
コントローラでやる事はこれだけです。
以下、変更点に関してなんとなく解説。
- 変更点 ①
まず、メソッドに org.springframework.ui.Model
を受け取るようにパラメータを追加しています。
SpringBoot先生は大変頭が良いので、我々が 「コレを受け取りたいよー」 ってメソッドパラメータに持たせておけば、渡せるものを渡してくれるのです。
この場合、 MVC:Controller
が MVC:View
までデータを渡すために使用する ui.Model インタフェース を受け取るように宣言しています。
- 変更点 ②
で、サーバ処理として LocalDateTime.now().format();
でサーバ側の現在時刻をもとに日付時刻の文字列を生成しています。
(フォーマットはめんどくさいので、定義済みの ISO_DATE_TIME
にしてますが、好きなフォーマットにして構わないです。実際のシステムでは「ログインユーザの設定で変更できる」なんて要件もありそうですよね。)
まぁ、ここは本題とはあまり関係ないので値が動的なら何でも良いです、何なら適当に UUID.randomUUID().toString();
とかでも良いです。
- 変更点 ③
そして model.addAttribute( "datetime", now );
で値を設定しています。
※ 今回、ここでは詳しく説明しませんが Model のほかに ModelAndView というものがあります。
(ざっくり概要だけ書いておくと、上記サンプル実装ではView名の指定とModelへのデータ設定が別れていますが、これを単一のオブジェクトへの操作とするのがModelAndViewさんです)
では、Controllerの実装は終わったので、次にViewの方に手を入れましょう。
> What time is it 画面HTML の実装
Viewの方はもっと簡単です。
<body>
<h1 th:text="'■What time is it?■'"></h1>
<div class="border">
<p th:text="${datetime}"><p>
</div>
</body>
これだけ。
追加したのは <p th:text="${datetime}"><p>
の部分。
タグは <p>
にしたけど、別に今は表示出来れば何でも良いです。
説明の前に、動きを見てみた方がテンション上がると思うので、前回の index.html
の時同様、SpringBootApplicationを起動したら http://localhost:8080/view/what-time-is-it
にアクセスして実際にブラウザで見てみましょう。
ハイ、Viewテンプレートの <p th:text="${datetime}"><p>
部分に、見事サーバ側の日付時刻 (つってもlocalhostだけどw) が表示されましたね。
- 軽く解説
ここで指定してる datetime
というのは、さっきコントローラで model.addAttribute( "datetime", now );
した時の "datetime"
をキーに指定して、中身の値を引っ張り出してる訳です。
普通のJavaのコードで Map.get( "datetime" );
って書いたようなもんです。
要するにContorollerで Map.put( "datetime", now );
したものをViewが Map.get("datetime");
して取り出すようなイメージ。
コードの字面が model.addAttribute( "datetime", now )
と ${datetime}
に変わっただけです。
- EL式
さて、この Map.get
みたいなことをしている ${}
コイツは一体何者なんでしょうか?
コイツは 式言語 、別名 EL式 と呼ばれるものです。
■式言語:SpEL - Spring Expression Language
> EL式(SpEL式)の概要
- 概要
SpringBoot+Thymeleaf
での開発では、データモデルmodel
からビューview
にデータを渡す(データバインドする)には SpEL式という仕組み を利用します。
JavaEE(JSF)時代に、ManagedBeanからxhtmlにデータバインドするのにEL式を触ってた人なら馴染み深いかと思います、 あんな感じのアレ です。
※ EL式自体はかなり昔からある技術で、Spring固有のものではないです。
SpringのEL式 として拡張実装されたものなので、正式名称としては 「SpEL式」 というようですが、別にここはそんなに重要じゃないので普通に 「EL式」 で良いでしょう。
- EL式 is 何?
EL式を使った経験のない人向けにざっくり説明すると、さっきの model.addAttribute で仕込んでおいたデータを、ビューであるHTMLに埋め込む時に使ったのが EL式 です。
この EL式のルールに則って記述した部分 は、HTMLのビューテンプレート上で 特別な意味を持つ ようになり、テンプレートエンジン (今回の場合はThymeleafさん) によって処理されます。
- さっきの What time is it での具体例
具体的には <p th:text="${datetime}"><p>
の部分ですね。
${datetime}
この部分がEL式になっており、datetime
という識別子(キー)のデータに置き換えて貰う、という記述になっています。
つまり、model.addAttribute( "datetime", now )
で与えたデータ(この場合は 2019-12-04T16:40:10.5164923
という文字列ですね)が使用されます。
そして、その置き換えたデータが th:text
というThmeleafの独自属性に与えられるので、テンプレートエンジンがHTMLをレンダリングする時にこのデータを使用して <p>タグ
のテキスト要素として埋め込まれた訳です。
要するに、EL式とは Thymeleaf先生とお話するための言語 ですね。
> EL式のいろいろな機能
EL式の最もシンプルで基本的な使い方が、先程の例のような「addAttributeで埋め込んでおいたデータを取り出す」でしょう。
勿論EL式には他にも様々な機能があります。
条件判定 ができたり、Mathクラスなどの staticメソッドを利用 できたり、他にもリクエストパラメータparam
やセッションsession
や下記ユーティリティなど よく使うオブジェクトがデフォルトで提供 されていたりします。
-
Thymeleafチートシート#ユーティリティオブジェクト - Qiita
#strings
#numbers
#bools
#dates
#calendars
#objects
#arrays
#lists
#maps
#sets
これだけ色々と揃っていれば、バックエンドのロジック無しでもEL式だけでも、多少の機能なら作れちゃいますね。
> 4つの式 [変数式/メッセージ式/リンク式/選択変数式]
ちなみに、先程説明した ${datetime}
というEL式ですが、これはThymeleafに於ける式四天王 「標準式構文」 のひとり 変数式
というものです。
- 標準式構文
${変数式}
*{選択変数式}
#{メッセージ式}
@{リンク式}
それぞれざっくり説明すると、こんなかんじ。
- 変数式
既に説明したとおり、埋め込んでおいた変数を参照する式。
この辺を更に詳しく知りたい人は OGNL
とかのワードでググると良いでしょう、ここでは省略します。
- 選択変数式
th:object
や th:field
と合わせて使用する、オブジェクトのショートカットポインタみたいな式。
例えば th:object="$(hoge)"
として hoge
をオブジェクトに仕込んでいるタグ内で、 *{id}
とすると ${hoge.id}
相当、*{name}
とすると ${hoge.name}
相当の記述になるようです。
なんかあれですね、VBの WITHステートメント
みたいなアレですね。
- メッセージ式
これは割と解かりやすい、プロパティファイルの外部定義値をバインドするのに使用するものです。
こkれが「メッセージ」という名前になってるのは、一般的に多言語対応とかで message.properties
がよくいるから、ですかね?
- リンク式
超便利なのがこのリンク式。
これはコンテキストパスを補完して良い感じにURLを記述できるスグレモノです。
何より便利なのが、パス変数を使って 動的なURLを表現する事も出来る ということ。
例えば @{/resource/{id}/detail(id=${resourceId})}
みたいな事ができます。
この記述を細かく見ていくと /resource/{id}/detail
という相対パスがあり、このパスの中には {id}
という動的パス要素が含まれており、そのidには ${resourceId}
の変数式で参照した値を使う、、、と言う感じです。
■画面をイケメンにしてみよう。
これは趣味ツールの画面なんですけど。
What time is it?
画面はお化粧なしのHTMLすっぴん画面でしたが、Thymeleaf に Bootstrap を組み合わせれば簡単にイケメンは作れます。
> pomにBootstrapの依存関係を追加
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.5.2</version>
</dependency>
> ViewテンプレートにBootstrapのCSSを適用
<link rel="stylesheet" th:href="@{/webjars/bootstrap/4.5.2/css/bootstrap.min.css}">
後は普通にBootstrapを利用すれば良き。
ちょっとした管理画面とかなら Thymeleaf+Bootstrap
で十分だと思うんだよなぁ。
★Thymeleaf画面作成の参考ページ★
EL/SpEL式 関連
- SpringFramework公式(英語)
- Spring Boot で Thymeleaf 使い方メモ - Qiita
- Thymeleafチートシート - Qiita
-
必要最小限のサンプルでThymeleafを完全マスター - Instructor's memo
- 4つの記法
- Thymeleafの標準式構文 - Java好き
- Spring BootでThymeleafを使ってみよう!(応用編)#記述方法 - Marinroad
Thymeleaf+Bootstrap 関連
- Bootstrap
- SpringBootアプリにBootstrap4を追加(WebJars使用) - One IT Thing
- SpringBoot入門 vol.13:Bootstrapでスタイルを整えよう - プログラミング逆引き辞典
- Spring Boot ThymeleafにBootstrapを追加 - ITSakura
もちろん、他にも ヘッダフッタの共通化 とか パーツの部品化 とか色々出来るので、そのへんの作り込みは助走本では割愛します。
【スケジュール機能を使ってみよう編】
突然なんですが、ぼくはかなりの ツイ廃 でして。
なんかもう呼吸の如くツイートするし、もはや自分でツイートするのも面倒で機械的にツイートするくらいになりました。(病気)
以前、趣味で作ったツールがあって、それをSpringBootでリライトしたものがあるんですが、それに 時報モドキ の機能があります。
にゃっぴー。
— エル(絵描きで技術者でカレー屋) 🥐🍙🏴☠️☃️ (@ellnore_pad_267) September 30, 2020
10月1日の 01時 ですよー。
2020-10-01T01:00:00.0027646 #にゃっぴこーる
こんな感じで #にゃっぴこーる
というタグを付けて、毎時ちょうどに時報をぶっ放しています。
ちなみに、たまに時報以外に切り替えて別なことをツイートするので、時報モドキとなっています。
まぁそれは置いといて。
この時報ツイートは SpringBootのスケジュール機能 を使っています。
■スケジュール機能を有効化してみよう。
スケジュール機能を利用するには、裏でタイマーを走らせる必要があるので明示的に 機能の有効化 が必要になります。
と言っても別に難しい実装が必要なわけじゃなく、ぶっちゃけ アノテーション1個付けるだけ です。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class T4jBootApplication {
public static void main(String[] args) {
SpringApplication.run(T4jBootApplication.class, args);
}
}
こんな感じで @EnableScheduling でアノテートすると、スケジュール機能を使えるようになります。
確かこの有効化アノテーションを付けるのはアプリケーション配下の (スキャン範囲に入っていれば) どこでも良いらしいんですが、
この手の有効化アノテーションは一箇所に纏まってたほうが解り易い (管理しやすい) と思うので、
ぼくはいつも @SpringBootApplication
をアノテートしているアプリケーションクラスに纏めて付けています。
同様の有効化アノテーションの例として、非同期処理を行う @Async
を使うための @EnableAsync
というものがあります。
■スケジュール機能を使ってみよう。
これでスケジュール機能を利用する準備が整いました。
ということで実際にスケジュール機能を使ってみましょう。
適当なRestControllerや任意のComponentクラスを作って、定期的に実行したいメソッドに @Scheduled
アノテーションを付与します。
これはもうサンプルコードを見た方が圧倒的に理解が早いと思うので、さっさと参考実装を見てみましょう。
参考実装
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class NyappiScheduler {
@Autowired
NyappiCall nyappi;
@Scheduled(cron = "${schedule.nyappi_call.cron}")
public void cron() {
this.nyappi.randomcall();
}
}
schedule.nyappi_call.cron=0 0 * * * *
Spring大先生のアノテーションは神ですので、外部定義ファイルの設定値でアノテート出来ます。
上記参考実装の場合、外部定義ファイルの schedule.nyappi_call.cron
に設定している 0 0 * * * *
という cronライクな記法 に従って定期実行されます。
つまりこの場合は 「毎時0分0秒に実行しなさい」 ということですね。
(もちろん、外部定義ファイルを使用せず、直接リテラルで指定しても構いません)
なお、@Scheduled
アノテーションに指定できるのはcron記法だけではなく、以下のようなパラメータを指定できます。
- cron系
- cron
- second ※ SpringのScheduled.cronには「秒」フィールドが必要です、crontabに慣れてる人は注意かも。
- minute
- hour
- day of month
- month
- day of week
- zone
- cron
- 一定間隔系
- initialDelay
- fixedRate
- fixedDelay
- initialDelayString
- fixedRateString
- fixedDelayString
> cron系
参考実装で示した通り、cronライクな記法で 実行スケジュールを曜日月日時分秒で指定 するパターンです。
なお zone
は cron
とセットで指定するプロパティで、cronスケジュールの実行タイムゾーンを指定するものですね。
> 一定間隔系
一定間隔系は大きく 2種類 に別れます。
-
fixedRate
一定周期でタスク実行 -
fixedDelay
一定間隔でタスク実行
どう違うねん!!
どちらも一定のスパンでタスク実行される点では同じですが、その 判定基準 が異なります。
-
fixedRate
前回タスク開始 を基準に次のタスクを実行。 -
fixedDelay
前回タスク終了 を基準に次のタスクを実行。
つまり 「タスクとタスクの間隔が一定;fixedDelay」 なのか、 「タスク毎の開始間隔が一定;fixedRate」 なのか、ということですね。
ちなみに、タスク自体の実行時間が長くて実行間隔を超えるような場合はタスクが詰まりますが、話が複雑になるのでここでは割愛します。
前述の @Async
を併用するなどして、上手いこと設計しましょう。
initialDelay
initialDelay
は fixedRate/fixedDelay
に付属するオプションみたいなもので、 アプリケーションをデプロイしてから初回実行までの間隔 を指定します。
■スケジュール(定期実行タスク)機能に関する余談
実際のシステム開発に於いては スケジュール(定期実行タスク) を実装する上では以下の選択になるかと思います。
- Scheduledアノテーション使って内部的にcronを回す。
- エンドポイントを用意しといて外部でcronを回してcurl叩く。
個人的には、後者 の方がスケジュールのハンドリングや管理のしやすさという意味で良いかなと思ってます。
基本的に、実務でのシステム開発ではほぼ後者を選択する事になるかなと思います。
よくあるケースとして運用部門の JP1
とか SystemWalker
とかの方が主導権握ってて、そっちからキックするのが既定路線だったりしますしね。
趣味ツールに於いても、定期実行した結果をファイルに蓄積して、、、みたいな、いわゆるクローラ的な事をやる上でも後者の方が好都合ですし。
特にそういうバッチ処理的な色合いがなく、シンプルに 定期的にアクションを起こしたいだけ みたいな時に手軽に実装出来るのが @Scheduled
アノテーションのいい所ですね。
外部にキックする「頭脳」を作らず、一人で勝手に動いてくれるのでラクに実装できるのがメリットですかね。
つまり、、、実務ではあまり使わない可能性が微レ存・・・?
あと、実際には @Scheduled
だけで事足りる事はなく @Async
でワーカースレッドと組み合わせるのが現実的かと思います。
★スケジューラ関連の参考ページ★
ついでなんで趣味ツールのGitHubリポジトリも参考実装として載せときます。
- 参考情報
- 参考実装
ちなみに、、、。
まぁ、当たり前の話なんですが。
タイマーの分解能とかはデプロイしている実行環境のマシンスペックに依存するので、処理が実行されるタイミングの正確さにはある程度バラツキが生じます。
【応用編:Thymeleafをプレーンテキストで使ってみよう編】
さて、スケジューラと順番が前後しましたがThymeleafのお話に戻ります。
■Thymeleafとはそもそもなにか?
Thymeleafは 汎用のテキストテンプレートエンジン です。
汎用 と言っているからには、用途はWeb画面 (HTMLテンプレート) だけではありません。
Thymeleafを使っていれば、単なる定型文 (プレーンテキスト) に対しても利用出来るので、
いわゆる メールテンプレート のような機能が必要になった場合も簡単に実装できます。
とにかくまず 「動くコードを見て、動きを理解する」 というのが 助走本のモットー ですので。
取り敢えずさっさとベタ書きでも動くコードを見てみましょう。
■とにかくまずはプレーンテキストで使ってみよう!
専用のネタを考えるのが面倒だったので、今回も趣味ツールの #にゃっぴこーる
氏を題材にしてみます。
(この話の流れにするために、章構成を前後する必要が、あったんですねぇ)
スケジュール機能の方でも話題に出したこの 時報モドキ 機能。
最初はただの文字列ベタ書きのひっでぇ泥実装だったんですが、今はThymeleaf対応しています。
必要なのは以下の2点
- テンプレートファイルを自作する。
- テンプレートエンジンのインスタンスを生成して使う。
です。
>テンプレートファイルを自作する
とりあえず、にゃっぴこーるの 時報ツイートのテンプレートファイル はこうなっています。
にゃっぴー。
[[${hour}]]ですよー。
[[${timestamp}]] #にゃっぴこーる
このファイルをですね、取り敢えず src/main/resource/templates
の下に messages
ってフォルダを掘ってブチ込みます。
で、まぁ見たら想像つくと思いますが [[${識別子}]]
の部分が プレースホルダ です。
アノテーションのあたりでもよく ${識別子}
ってのはよく出て来るので、皆さんもうお馴染みのアレですね。
ここではなんとなく hour
及び timestamp
という識別子でデータバインドするんだなー、って事を何となく理解してくれればよきです。
> テンプレートエンジンを使って文字列化する
では、上記テンプレートファイルを実際にデータバインドしてテンプレート処理するサンプルコードを見てみましょう。
まずはシンプルに そのままコピペして動く レベルの雑なベタ書きコードを載せます。
適当な @RestController
にでも貼り付けて動かして下さい。
// これはただの動作確認用
@GetMapping("test")
public String test() {
LocalDateTime now = LocalDateTime.now();
String hour = now.getHour() + "時";
String timestamp = DateTimeFormatter.ofPattern( "yyyy/MM/dd HH:mm:ss" ).format( now );
return process( hour, timestamp );
}
// 重要なのはこっち。
private String process( String hour, String timestamp ) {
var resolver = new ClassLoaderTemplateResolver(); // ①
resolver.setTemplateMode( TemplateMode.TEXT );
resolver.setPrefix( "templates/messages/" ); // src/mail/resources/templates/messages
resolver.setSuffix( ".message" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( true );
var engine = new SpringTemplateEngine(); // ②
engine.setTemplateResolver( resolver );
var context = new Context(); // ③
context.setVariable( "hour", hour );
context.setVariable( "timestamp", timestamp );
final String message = engine.process( "nyappi_call", context ); // ④
log.debug( message );
return message;
}
ではざっくり実装内容を見ていきます。
せっかちさんは 「実行結果」 を載せておくので、そこまで飛んで下さいw
- ①
new ClassLoaderTemplateResolver()
まず作っているのが ClassLoaderTemplateResolver
テンプレートリゾルバ と呼ばれるものです。
Resolver;リゾルバ
とは 「解決するもの」 なので、
テンプレートリゾルバとは 「テンプレートファイルを解決するためのもの」 ですね。
なるほどわからん、という人は みんな大好きWikipedia先生 に聞こう!!
> リゾルバ(Resolver)とは - Wikipedia
まぁ、なんかカッコイイ名前を名乗ってやがりますが、
要するに テンプレートファイルの置き場所やなんかを指示してやる設定オブジェクト ですね。
実際に設定している内容を見てみれば 「ふ~ん、はいはい、なるほどね」 となるでしょう。
設定 | 設定値 | 説明 |
---|---|---|
TemplateMode | TemplateMode.TEXT |
テンプレートモード。※ |
Prefix | "templates/messages/" |
プリフィックス、要するにフォルダ。 (/src/main/resources からの相対パス) |
Suffix | ".message" |
サフィックス、要するに拡張子。 |
CharacterEncoding | "UTF-8" |
文字エンコーディング |
Cacheable | true |
キャッシュの有効/無効 |
なるほどなるほど、、、Web画面実装編で @Controller
が最期に 「View名をreturnしていた」 意味が何とな~く見えてきましたね。
-
※org.thymeleaf.templatemode.TemplateMode
- HTML
- XML
- TEXT
- JAVASCRIPT
- CSS
- RAW
-
HTML5@deprecated -
LEGACYHTML5@deprecated -
XHTML@deprecated -
VALIDXHTML@deprecated -
VALIDXML@deprecated
ちなみになんですけど。
ClassLoaderTemplateResolver
コイツ、結構継承階層が深くて。
- ITemplateResolver
- AbstractTemplateResolver
- AbstractConfigurableTemplateResolver
- ClassLoaderTemplateResolver
- AbstractConfigurableTemplateResolver
- AbstractTemplateResolver
って感じなんですよね。
実装上も ITemplateResolver
で受けたい所ですが、設定するプロパティが案外深いところで実装されてるので、ここは素直に var
で受けてます。
更にちなみになんですけど。
ぼくはもともとC#屋さんなので全く違和感ないんですけど、
Javaでinterfaceに ITemplateResolver
って Iプリフィックス 付けてるの珍しくないですか?
個人的にはこれ凄く好きです。
というか XxxXxxImpl
っていう Implサフィックス文化 が嫌いです。
(まぁでも、一番アレなのは一貫性がないことだよなぁ?)
- ②
new SpringTemplateEngine()
お次が SpringTemplateEngine
主役の テンプレートエンジン と呼ばれるものです。
実行しているのは engine.setTemplateResolver( resolver );
で、さっき作ったテンプレートリゾルバを設定しているだけです。
こいつが、与えられたテンプレートリゾルバの設定に従って、テンプレートファイルを探しに行って色々やってくれるって事でしょう。
解りやすいですね。
- ③
new Context()
お次が Context
コンテキスト ですね。
コンテキスト?文脈?はて??
なんてことはない、ただの パラメータ ですね。
ここではテンプレートのプレースホルダに対応して、以下のパラメータを与えているだけです。
-
[[${hour}]]
:context.setVariable( "hour", hour );
-
[[${timestamp}]]
:context.setVariable( "timestamp", timestamp );
-
④
engine.process( "nyappi_call", context );
第一オペランドに テンプレート名 を、第二オペランドに パラメータ(Context) を指定して SpringTemplateEngine#process
を実行します。
こうすると、
- TemplateEngine;テンプレートエンジン に与えておいた TemplateResolver;テンプレートリゾルバ を使って、
- 指定された テンプレート名 に対応 (= プレフィックス + サフィックス を付与したパスにマッチ) する テンプレートファイル を探しに行き、
- 与えられた パラメータ でテンプレート処理した結果を返してくれる。
という流れですね。
- 実行結果
$ curl -XGET -H "Content-Type: application/json" 172.19.32.1:8989/nyappi/api/test
にゃっぴー。
20時ですよー。
2020/10/29 20:02:28 #にゃっぴこーる
ログにもこんな感じで出ています。
2020-10-29 20:02:28.256 DEBUG 10532 --- [nio-8989-exec-2] s.t.a.controller.rest.TestApiController : にゃっぴー。
20時ですよー。
2020/10/29 20:02:28 #にゃっぴこーる
良い感じですね。
■TemplateEngineをBean登録して使おう。(その1)
さて、先のコピペ用の参考実装で気になった人もいるでしょう。
この ClassLoaderTemplateResolver
と SpringTemplateEngine
、都度都度 new
しなくてよくね?
何らかの形で シングルトン にするなり、 コンテナ管理Beanとして登録 してオートワイヤリングするなり、何かしたいですよね?
ということで、雑に Bean定義 してコンテナに突っ込んじゃいましょう。
実装内容はさっきのサンプルコードの テンプレートエンジンのインスタンスを生成する所まで ですね。
@Bean()
public SpringTemplateEngine messageTemplateEngine() {
var resolver = new ClassLoaderTemplateResolver();
resolver.setTemplateMode( TemplateMode.TEXT );
resolver.setPrefix( "templates/messages/" ); // src/mail/resources/templates/messages
resolver.setSuffix( ".message" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( true );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
何気に、助走本の説明では初登場の @Bean
アノテーションさん。
一言でいうと 「コンテナ管理Beanのファクトリメソッドであること」 を示すアノテーションです。
このファクトリメソッドを使って生成したインスタンスを Springコンテナさんが管理してくれる ようになります。
取り敢えず 「@Bean
でアノテートしておくと @Autowire
でインジェクションして使えるよ」 ということだけ理解しとけば取り敢えず良きです。
※ この「Bean定義」はSpringフレームワークを使用する上で極めて重要な概念になります。後々、詳しく書こうかと思いますが、今はこのくらいで。
で、このコードを 適当なところ に実装して下さい。
適当なって言われても・・・って人は、一旦取り敢えず @SpringBootApplication
でアノテートしているApplicationクラスに実装しちゃって下さい。
ほんで、さっきのサンプルコードを以下のように変更します。
@Autowired
SpringTemplateEngine engine; // ⑤
@GetMapping("test")
public String test() {
LocalDateTime now = LocalDateTime.now();
String hour = now.getHour() + "時";
String time = DateTimeFormatter.ofPattern( "yyyy/MM/dd HH:mm:ss" ).format( now );
return process( hour, time );
}
private String process( String hour, String time ) {
var context = new Context();
context.setVariable( "hour", hour );
context.setVariable( "timestamp", time );
final String message = this.engine.process( "nyappi_call", context ); // ⑥
log.debug( message );
return message;
}
変更点は二箇所。
- ⑤
@Autowired SpringTemplateEngine
テンプレートエンジンを @Autowire
でフィールドインジェクションするよう変更。
- ⑥
this.engine.process
さっきは #process
メソッド内で生成していたテンプレートエンジンを、インジェクションしたコンテナ管理Beanを使うように変更。
- 実行結果
$ curl -XGET -H "Content-Type: application/json" 172.19.32.1:8989/nyappi/api/test
にゃっぴー。
22時ですよー。
2020/10/29 22:05:58 #にゃっぴこーる
はい、上手く行った・・・
と思ったか?
この状態で、画面にアクセスしてみてください。
There was an unexpected error (type=Internal Server Error, status=500).
An error happened during template parsing (template: "templates/messages/index.message")
画面が出なくなってるやないけぇ!?!?
画面なんか弄ってないのに何で動かなくなるの!!
■TemplateEngineをBean登録して使おう。(その2)
さて、困った事に (その1) でテンプレートエンジンをBean登録して使ったら何故か画面がバグってしまいました、 バタフライエフェクト かな?
An error happened during template parsing
つってるんで、テンプレートをパースする時にコケたぜって言ってますね。
で、コケたテンプレートは templates/messages/index.message
だと言っています。
ん?
なんすかこいつ。
画面なんだからお前は templates/index.html
じゃないのかと。
もしかして templates/messages
で .message
って、こ、これは、さっき作ったテンプレートリゾルバの設定じゃないか! (わざとらしい)
どうやら、 もともとHTML用のテンプレートエンジンが動いていた所に巻き込む形で、さっき作ったテンプレートエンジンを差し込んでしまった ようです。
ということで、まずは先程のコードをこのように修正して下さい。
// ⑦
@Bean
public SpringTemplateEngine pageTemplateEngine() {
var resolver = new ClassLoaderTemplateResolver();
resolver.setTemplateMode( TemplateMode.HTML );
resolver.setPrefix( "templates/" ); // src/mail/resources/templates
resolver.setSuffix( ".html" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( false );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
@Bean("messageTemplateEngine") // ⑧
public SpringTemplateEngine messageTemplateEngine() {
var resolver = new ClassLoaderTemplateResolver();
resolver.setTemplateMode( TemplateMode.TEXT );
resolver.setPrefix( "templates/messages/" ); // src/mail/resources/templates/messages
resolver.setSuffix( ".message" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( true );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
インジェクションしていたところもこのように変更。
@Qualifier("messageTemplateEngine") // ⑨
@Autowired()
SpringTemplateEngine engine;
変更点を纏めてざっくり。
- ⑦ HTML用に設定したテンプレートエンジンのBean定義を追加。
- ⑧ プレーンテキスト用のテンプレートエンジンのBean定義に明示的に管理名を設定。
- ⑨
@Autowire
インジェクションポイントに、⑧で設定した管理名を@Qualifier
指定。
で、これで動かしてみると、
なんとまだエラーが出ます。
なかなか見ることのない珍しいエラーだと思うので、まるっと拾ってきました。
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of method thymeleafViewResolver in org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration$ThymeleafWebMvcConfiguration$ThymeleafViewResolverConfiguration required a single bean, but 2 were found:
- pageTemplateEngine: defined by method 'pageTemplateEngine' in class path resource [sugaryo/t4jboot/SpringBeans.class]
- messageTemplateEngine: defined by method 'messageTemplateEngine' in class path resource [sugaryo/t4jboot/SpringBeans.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
実に親切なエラーメッセージで感動しますよね!!
これを載せたかったがために、この章では回りくどい説明をしていました。
ということで有り難いエラーメッセージ様を読んでみましょう、なになに?
Parameter 1 of method thymeleafViewResolver in ほにゃらら required a single bean, but 2 were found:
「ほにゃらら.thymeleafViewResolverメソッドでは単一のBeanが必要だが2個指定してんじゃねえかおめぇ」 と。
[sugaryo/t4jboot/SpringBeans.class] で定義されてる
pageTemplateEngine
コイツ[sugaryo/t4jboot/SpringBeans.class] で定義されてる
messageTemplateEngine
コイツ
「2個指定されたのはこれとこれだぞ」 と。
なるほどなるほど。
で、有り難いことにエラーに対する修正アクションが提案されています、神かな?
Consider marking one of the beans as @Primary,
updating the consumer to accept multiple beans,
or using @Qualifier to identify the bean that should be consumed
- どれか一つのBeanに
@Primary
を付けるか - 複数の
@Bean
が扱えるように修正するか - 注入する方で
@Qualifier
でBean管理名を明示するか
いずれかを検討しろと。
要するに 「複数のBean(この場合テンプレートエンジン)が見付かってどれ使えばいいか解らんから、どれを使えばいいか解るようにせい」 ということですね。
で、今回は @Primary
を付けることでエラーを回避します。
@Primary // ⑩
@Bean
public SpringTemplateEngine pageTemplateEngine() {
var resolver = new ClassLoaderTemplateResolver();
resolver.setTemplateMode( TemplateMode.HTML );
resolver.setPrefix( "templates/" ); // src/mail/resources/templates
resolver.setSuffix( ".html" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( false );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
@Bean("messageTemplateEngine") // ⑧
public SpringTemplateEngine messageTemplateEngine() {
var resolver = new ClassLoaderTemplateResolver();
resolver.setTemplateMode( TemplateMode.TEXT );
resolver.setPrefix( "templates/messages/" ); // src/mail/resources/templates/messages
resolver.setSuffix( ".message" );
resolver.setCharacterEncoding( "UTF-8" );
resolver.setCacheable( true );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
- ⑩ Thymeleafに使って貰うHTML用のテンプレートエンジンの方に
@Primary
を付与。
で、これで動かすと、
漸く正しく動くようになります。
■TemplateをDBに文字列で持ちたいです!
はい、ちょっと脱線しましたが、これでプレーンテキストモードでのテンプレートエンジンを利用出来るようになりました。
これで src/mail/resources/templates/messages
にテンプレートファイルをばしばし突っ込んでおけば、いろんなテンプレートを使えます。
はい、そうですね。
ここまで来るとテンプレートファイルを用意するのすらめんどくさい、テンプレートをファイルじゃなくDBか何かで文字列としてそのまま持っておくようにしたくなるのが人情ですよね。
Thymeleaf3系から追加された StringTemplateResolver というリゾルバを使えば行けるようです。
早速試してみましょう。
import org.thymeleaf.templateresolver.StringTemplateResolver;
@Bean("stringTemplateEngine")
public SpringTemplateEngine stringTemplateEngine() {
var resolver = new StringTemplateResolver();
resolver.setTemplateMode( TemplateMode.TEXT );
var engine = new SpringTemplateEngine();
engine.setTemplateResolver( resolver );
return engine;
}
デフォルトのテンプレートモード DEFAULT_TEMPLATE_MODE
が TemplateMode.HTML
らしいので、TEXTモードだけ変更してリゾルバを設定しました。
@Qualifier("stringTemplateEngine")
@Autowired
SpringTemplateEngine string;
@GetMapping("test/string/{message}")
public String test_string_template(@PathVariable String message) {
final String template = "path var message<[[${message}]]>, by string template engine.";
var context = new Context();
context.setVariable( "message", message );
return this.string.process( template, context );
}
"path var message<[[${message}]]>, by string template engine."
というテンプレートを文字列リテラルで定義し、パス引数を埋めて返すだけの簡単なお仕事。
$ curl -XGET -H "Content-Type: application/json" 172.19.32.1:8989/nyappi/api/test/string/hogehoge
path var message<hogehoge>, by string template engine.
良い感じですね。
例えばこれで、メールテンプレートをマスタ管理しておいて、、、みたいなことも簡単に出来そうですね。
テンプレートの変更頻度にも依存しますが、DB管理しといた方が変更を反映しやすいので便利ですよね。
★Thymeleafテンプレートエンジン関連の参考ページ★
- Thymeleaf公式ドキュメント
【ちょっとしたTips集】
章建てするほどの事もない小ネタや、自分が過去にハマって困った事など。
- Tips内目次
■自分の書いたプログラムのデバッグログを出す方法
ログ自体の設定(application.properties
や logback-spring.xml
)の話ではなく、ログレベルの設定の話。
アプリ全体のログレベル設定じゃなく、自分の書いたプログラムのログレベル設定にぷちハマったので書いておきます。
結論から言って普通に application.properties
に設定出来るんですが 「えっ、そんなのアリなの?」 とちょっと意外だったので。
ルートレベルから指定する方法とかは普通にサラッと紹介されてると思うんですが。
logging.level.root=INFO
これだと自分の組んだプログラムにDEBUGレベルが適用できなかったり、
そもそもルートにDEBUGレベル指定するとどっかで永久ループハマって立ち上がらなかったり、
なんか意味解らん動きをしたんですけど、細かい説明が書かれてる記事が無くて困ったんですよね。
で、自分のプログラムのDEBUGログを有効にするにはどうやらこうするようです。
logging.level.root=INFO
logging.level.sugaryo.t4j.demo=DEBUG
! ^^^^^^^^^^^^^^^^
! この部分は自分のアプリケーションの名前空間になります
この設定だと自分のアプリケーション全体にログレベルが掛かりますが、更にピンポイントで設定も出来るようです。
この設定で実際にログを出してみました。
log.info( "INFO ログ" );
log.debug( "DEBUG ログ" );
log.warn( "WARN ログ" );
log.error( "ERROR ログ" );
2019-05-16 03:09:46.617 INFO 10036 --- [nio-8080-exec-1] s.t4j.demo.controller.WebApiController : INFO ログ
2019-05-16 03:09:46.617 DEBUG 10036 --- [nio-8080-exec-1] s.t4j.demo.controller.WebApiController : DEBUG ログ
2019-05-16 03:09:46.617 WARN 10036 --- [nio-8080-exec-1] s.t4j.demo.controller.WebApiController : WARN ログ
2019-05-16 03:09:46.617 ERROR 10036 --- [nio-8080-exec-1] s.t4j.demo.controller.WebApiController : ERROR ログ
はい、こんな感じでちゃんとDEBUGレベルでログが出てますね。
この辺ってSpringFramework経験者からすると常識なんでしょうけど、SpringBootから入った自分にはハマりポイントでした。
■実行可能JARとして設定してビルドする
実装中に動作確認する意味で動かすには、前述の通り Run AS > Spring Boot Application
メニューで起動すると思いますが、リリース用とかで実行可能JARをビルドしたいだけの場合。
びっくりしたんですが、デフォルトのままだとダメでした。
実行可能JARとしてビルドするため pom.xml
に以下の設定追加が必要になります。
build.plugins.plugin.configuration.executable = true
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
こいつを
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
こうします。
この設定をやっておかないと、CentOSに持ってって service
コマンドでサービス登録して動かそうと思った時に上手くいきませんでした。
あとは Run As > Maven build
でビルドするとJARが出来上がるんで、持ってってください。
■pom.xmlの一行目に謎のエラー出んだけど!!
上の方の 環境構築編
にも書いた通りですが、最新のSTS4(少なくともぼくが確認したのは4.4.0)では解消してました。
軽く調べてみると m2e connector
のバージョンを変える事で発生しなくなったって人が結構いましたが、STS4本体側で何かしら修正が入ったようです。
(このエラー、出ていても無視して突き進んで問題なかったけど、気持ち悪いは気持ち悪かったから、消えて良かったね)
ということでSTS4のバージョン上げれば消えるよ。
■Swagger使ってみたいんだけど
取り敢えず最短でSwagger導入してswagger.ui(エンドポイント一覧の画面)を出すところまでやりたい、詳しい作り込みは後でやるから取り敢えずサクッと済ませたい。
っていう人向けの導入手順。
- pom.xmlに依存関係追加
- Swagger有効化
- Swaggerのコンフィグ設定
基本、以下の実装を適当にコピペしてけば動くことは動く。
> swagger 依存関係追加
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
> swagger 有効化
と言っても @EnableSwagger2 でアノテートするだけ。
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableSwagger2
public class SpringBootTipsApplication {
// 以下略
※ 後述するコンフィグクラスの方に付けても良いけど、ぼくは基本的にこの手の EnableXxx
系のアノテーション(スケジューラを有効化させる EnableScheduling
とかね)は全部Applicationクラスに付ける事にしてます。そこだけみれば何が有効化されてるのかがわかるように。
> swagger コンフィグ設定
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SwaggerConfig {
@Bean
public Docket doclet() {
return new Docket( DocumentationType.SWAGGER_2 )
.select()
.apis( RequestHandlerSelectors.any() )
.paths( PathSelectors.any() )
.build()
.apiInfo( Api.info() );
}
private static class Api {
private static ApiInfo info() {
return new ApiInfoBuilder()
.title( "タイトル" )
.description( "説明" )
.version( "バージョン" )
.contact( new Contact( "開発者・サイト名", "開発サイトURL", "メールアドレス" ) )
.license( "ライセンス" )
.licenseUrl( "ライセンスURL" )
.termsOfServiceUrl( "利用規約ページURL" )
.build();
}
}
}
ちなみに、ベタ書きなのは流石にアレなのでpropertiesファイルから読み込んで来る形にした実装サンプルを GitHubの方に上げてある ので、参考にどうぞ。
> swagger-ui 動作確認
http://localhost:8080/swagger-ui.html
にアクセスすれば例の画面がでてきます。
(ポート番号とかは各自のapplication.properties設定にあわせて読み替えて下さい)
コントローラ毎にアコーディオンパネルになってて、開くとこんな感じ。
前述のコンフィグ設定で、表示するしないの制御とかが細く設定出来るし、エンドポイント実装の方に @ApiOperation
などのメタ情報を埋めとけばその辺の情報を出すことも出来ます。
細かくは、詳しく解説してくれている記事を読んで下さい。
- 参考資料
■Service(というかTransactional)ロールバック仕様でハマったこと
> Transactionalのデフォルトのロールバック仕様
Serviceクラスには @Transactional
アノテーションを付けてトランザクションをコンテナ管理して貰いますが。
最初気付いた時びっくりしたんですけど、このTransactionalアノテーションは 何故か検査例外の場合にロールバックしない というのが デフォルト動作 だったようで。
しかもちょっと調べてみたら、正確には以下の動作だそうで。
- ランタイム例外が発生した場合は普通にロールバックされる。
- でも 検査例外 が発生した場合はロールバックされない。
- でも SQLException が発生した場合はロールバックされる。
- 正確にはSpringフレームワークがDB関連処理で発生する例外を springframework.dao.DataAccessException のサブクラスに置き換えており、この場合は特別にロールバックしてくれている模様。
なるほど、、、。
> Transactionalへのロールバック動作指定
Transactionalアノテーションには rollbackFor
というオプション指定があり 検査例外発生時にもロールバックさせたい場合 は、明示的に @Transactional(rollbackFor = Exception.class)
とする必要があります。
ちなみに。
ロールバック関連に関しては、上記のような Class<?>
を指定するタイプの rollbackFor
をはじめ、以下の4つの指定可能なパターンがあります。
- 例外発生時にロールバックさせたい例外クラスを
rollbackFor
に指定する。 - 例外発生時にロールバックさせたい例外クラス名を
rollbackForClassName
に指定する。 - 例外発生時にロールバック させたくない 例外クラスを
noRollbackFor
に指定する。 - 例外発生時にロールバック させたくない 例外クラス名を
noRollbackForClassName
に指定する。
Spring経験者にとっては常識なんでしょうけど、案外これってハマりポイントだと思うなぁ、、、。
- 参考情報
- 【Spring】@Transactionalは検査例外をコミットしてしまうがSQLExceptionはロールバックされる - じゃけぇのあれこれ
- Springでトランザクション管理 - Qiita
- DataAccess - Spring Framework Documentation
■JSONを SNAME_CASE で返す設定
@RestController
のメソッドからBeanを返すと、裏で jackson
先生が動いてBeanをJSON形式で返してくれますが。
デフォルトだとこれがJavaライクなキャメルケースのレスポンスになっちゃうんですよね。
これをJSONで一般的な CAMEL_CASE にしたいな。
でも @JsonProperty
でイチイチ指定するのもダルいし、、、。
> RestControllerのレスポンス設定
という時は application.properties (yml)
で以下のように指定すれば良きです。
spring.jackson.property-naming-strategy=SNAKE_CASE
> ObjectMapperのレスポンス設定
また、プログラム中に ObjectMapper を独自に使用している部分があれば、こちらも以下の設定でスネークケースに変更可能です。
ObjectMapper mapper = new ObjectMapper().setPropertyNamingStrategy( PropertyNamingStrategy.SNAKE_CASE );
勿論、スネークケースの他にもケバブやパスカル(アッパーキャメル)、小文字のドット繋ぎなんかも選択可能です。
> 参考
- 参考情報
- Gistコード
■JSONを PrettyPrint で出力する設定
JSONの整形出力、いわゆる pretty-print
ですね。
> SerializationFeature.INDENT_OUTPUT
jacksonのObjectMapper先生に SerializationFeature.INDENT_OUTPUT を食わせてやればよきです。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/** PrettyPrint設定のObjectMapper */
private static final ObjectMapper pretty = new ObjectMapper().enable( SerializationFeature.INDENT_OUTPUT );
この設定にしてやると、ObjectMapperでwriteValueAsStringしたJSON文字列が整形されて出て来ます。
多くの場合、整形するかしないかは api-endpoint-url?pretty
みたいな感じで オプション的に指定したい という要望が一般的なので、
PrettyPrint用のObjectMapperと通常出力用のObjectMapperで分けて使うことになるのかなと思います。
ぼくはこんな感じでシングルトンインスタンスを2個用意してます。
public static String stringify( Object obj, boolean pretty ) {
try {
return pretty
? SingletonHolder.pretty.writeValueAsString( obj )
: SingletonHolder.mapper.writeValueAsString( obj );
} catch ( Exception ex ) {
throw new RuntimeException( ex );
}
}
private static final class SingletonHolder {
// Initialization-on-demand-holder idiom
/** デフォルトのObjectMapper */
private static final ObjectMapper mapper = new ObjectMapper();
/** PrettyPrint設定のObjectMapper */
private static final ObjectMapper pretty = new ObjectMapper().enable( SerializationFeature.INDENT_OUTPUT );
}
> pretty オプションエンドポイントの参考実装
参考実装はこんな感じ。
@GetMapping(path="test/json")
public String test_json_n() {
return test_json( false );
}
@GetMapping(path="test/json", params="pretty")
public String test_json_p() {
return test_json( true );
}
private String test_json( boolean pretty ) {
@SuppressWarnings("serial")
Map<String, Object> map = new HashMap<String, Object>() {
{
put( "id", UUID.randomUUID() );
put( "name", "testdata" );
put( "value", 123 );
}
};
return JsonMapper.stringify( map, pretty );
}
同じエンドポイントパスにGetMappingしつつ pretty
のパラメータ指定ありなしで用意してみました。
それぞれ curl
叩いてみましょう。
$ curl -XGET -H "Content-Type: application/json" localhost:8989/t4j-boot/api/test/json
{"name":"testdata","id":"1f2a80e2-95b8-4064-bde8-bc2d22e13df4","value":123}
$ curl -XGET -H "Content-Type: application/json" localhost:8989/t4j-boot/api/test/json?pretty
{
"name" : "testdata",
"id" : "9e93d15e-430f-4efd-bfa2-e79cb28aae8e",
"value" : 123
}
良い感じですね。
> 参考
列挙体定義を見てみると解ると思いますが整形出力だけでなく色々設定出来るので、開発要件に応じて細かく調整すれば良いでしょう。
■本番運用系の便利エンドポイントを簡単に追加する
SpringBoot大先生には spring-boot-starter-actuator
という神機能がございまして。
これは素直に 公式ドキュメント を読むのが一番だと思うんですけど、
まぁ簡単に言うと システム運用で欲しくなりそうな一般的な管理エンドポイントを自動的に作ってくれる というスグレモノです。
なるほど、神だな。
なお、公式的にはこの機能は 「本番対応機能」 というようですね。
> actuator の有効化
公式に書いてある通り spring-boot-starter-actuator
の依存関係を追加します。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
ほんで、取り敢えず試しに application.properties
に以下を追記してください。
management.endpoints.web.base-path=/system
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=shutdown
management.endpoint.health.show-details=always
management.endpoints.web.xxx
系の設定が actuator エンドポイントに関する設定群です。
base-path
はエンドポイントのベースパスで、
デフォルトでは /actuator
になっているので変更したい場合はこれで指定します。
ぼくの場合 localhost:8989/system
が actuator エンドポイントのURLになります。
exposure.include
は有効化したい機能を指定するプロパティ、
exposure.exclude
は無効化したい機能を指定するプロパティ、
まぁ、見てそのまんまなのですぐ解ると思います。
health.show-details
は /health
エンドポイントのレスポンスを詳細に返す設定ですね。
提供されている機能エンドポイントとかは先の公式ドキュメントにも載っているので、それを確認してください。
> actuator エンドポイント
じゃ、実際動かしてアクセスしてみましょう。
取り敢えず actuator エンドポイントのベースパスにGETしてみます。
$ curl -XGET -H "Content-Type: application/json" localhost:8989/system
{"_links":{"self":{"href":"localhost:8989/system","templated":false},"beans":{"href":"localhost:8989/system/beans","templated":false},"caches-cache":{"href":"localhost:8989/system/caches/{cache}","templated":true},"caches":{"href":"localhost:8989/system/caches","templated":false},"health":{"href":"localhost:8989/system/health","templated":false},"health-path":{"href":"localhost:8989/system/health/{*path}","templated":true},"info":{"href":"localhost:8989/system/info","templated":false},"conditions":{"href":"localhost:8989/system/conditions","templated":false},"configprops":{"href":"localhost:8989/system/configprops","templated":false},"env":{"href":"localhost:8989/system/env","templated":false},"env-toMatch":{"href":"localhost:8989/system/env/{toMatch}","templated":true},"loggers":{"href":"localhost:8989/system/loggers","templated":false},"loggers-name":{"href":"localhost:8989/system/loggers/{name}","templated":true},"heapdump":{"href":"localhost:8989/system/heapdump","templated":false},"threaddump":{"href":"localhost:8989/system/threaddump","templated":false},"metrics-requiredMetricName":{"href":"localhost:8989/system/metrics/{requiredMetricName}","templated":true},"metrics":{"href":"localhost:8989/system/metrics","templated":false},"scheduledtasks":{"href":"localhost:8989/system/scheduledtasks","templated":false},"mappings":{"href":"localhost:8989/system/mappings","templated":false}}}
使えるエンドポイント一覧がでてきました、便利ですねぇ!!
管理機能はこうじゃないとなぁ!!
で、有効化されている各機能のエンドポイントを叩くと、色んなものが見れます。
/health
にアクセスすると、よくあるヘルスチェック結果(DB接続してる場合はDB情報も)が返ってきます。
/beans
にアクセスすると、SpringのDIコンテナが管理しているBean情報が見れます。
/mappings
にアクセスすると、コントローラで @RequestMapping系
アノテートしたURLマッピングの詳細情報が見れます。
/loggers
にアクセスすると、出力されているログが見れるのでこれはすごく便利ですね。
多くの場合、本番環境のログを見に行くったら結構面倒な手順を踏むと思いますが、これで雑に確認できるのはとても良いですね。
ただ、こうした内部情報を赤裸々に出してしまうので、きちんとセキュリティ保護を施す必要があります。
> 参考
- 参考情報
あとがき「SpringBootってなんだろう?」
(この記事のあとがきはいつ書かれるんですかねぇ・・・)