SpringBoot(1.2くらい)を使っていてハマった事
はじめに
Spring Boot 1.2.5 を使いはじめてハマった事をメモします。
既にご存じかとは思いますが、以下の Qiita がとても良くまとまっているので
ご紹介します。
#1.2.8 でも同じようにハマれるのを確認しています
SpringBoot(with Thymeleaf)チートシート[随時更新]
Whitelabel Error Page を置き換えたい
エラーメッセージだけを読むと、適当なコントローラーをつくって
/error のリクエストマッピングを作成すればよさそうに思えるが、
実際やってみると、マッピングが重複している的なエラーで起動すらできなくなる。
正解は、 /error をマッピングする Controller は ErrorController を
継承しなければならない。
こんな感じ。このコントローラーにエラーじゃないマッピングを含むことは OK。
当然だが、コンポーネントスキャンの範囲内に作らないとダメ。
<code class="language-java">@Controller
public class ErrController implements ErrorController {
private static final String PATH = "/error";
@RequestMapping("/404")
String notFoundError() {
return "error/404"; // templates/error/404.html
}
@RequestMapping(PATH)
String home() {
return "error/general";
}
@Override
public String getErrorPath() {
return PATH;
}
}
実は
/template/error.html を作れば自動的にそれが使用される。
※ 少なくとも Springboot 1.3.5 で確認
2017/01/27 追記
コメントでご指摘頂きましたが、 error.html を作成するとエラーページが置き換わるのは、Thymeleaf を
使用している時のみとのこと。 @syukai さんご指摘ありがとうございました。
独自の Formatter を追加したい
Formatter を作って、そのクラスに @Component を付けるだけ。
@ComponentScan 範囲にいれば、自動的に登録される。
ググると FormattingConversionServiceFactoryBean 使って登録みたいな
事が書いてあるが、Springboot では自動登録される。
#逆に、自動登録されては困るときは色々と考えないといけないんでしょう
Form Validation をしたいがフォーム View 表示の為に必要なデータがある
これ、意外とどこにも無くてハマったのですが、Form Validation は普通に
Springboot に付いてます。カスタムバリデーションも書けます。
それは良いけれど、フォーム画面を表示するのに model に何かをセットしないと
いけない場合の事は意外と書かれてません。これで正しいのかはわかりませんが、
フォーム表示時のメソッドを普通のメソッドとして呼び出せば上手くいきます。
親切なことに、フォームオブジェクト上のバリデーションに失敗した項目は
null が入っていますが、それを上書きして別の値を入れたとしても無視され、
入力した(結果エラーとなった値)が画面に表示されます。
// Controller
// フォーム表示
@RequestMapping(value="", method = RequestMethod.GET)
String showForm(@ModelAttribute TestForm form, Model model) {
// フォームに初期値をセット
form.setValue("default");
model.addAttribute("dbdata", hoge.getData());
return "testview";
}
@RequestMapping(value="", method = RequestMethod.POST)
String formPost(@ModelAttribute TestForm form, BindingResult result,Model model) {
if (result.hasErrors()) {
// ここでmodel.addAttributeしないといけないが、
// 処理としては普通に画面を表示するときと同じ場合なので同じことを書きたくない
// 良さそうな例
// 普通にメソッドとして呼べば良い
return showForm(form, model);
// ダメだったパターン(リダイレクト)
return "redirect:/testForm"; // 入力エラーの値が引き継がれない
// ダメだったパターン2
return "redirect:testview"; // modelの値が引き継がれない(当然)
}
(略)
return "completeview";
}
<form method="post" th:action="@{/test}"
th:object="${testForm}">
<input type="text" name="value" class="form-control"
th:field="*{value}" />
エラー表示は省略。
</form>
ControllerAdvice で例外を拾って処理をしたらステータスコードが 405
@ResponseBody の付け忘れ。これが付いてないとテンプレートを探しに行って
しまっている模様。
`o.s.web.servlet.PageNotFound : Request method 'POST' not supported
これ、 @ResponseStatus を平然と無視するのでかなり焦った。
RestController で Ajax でリクエストする API を作ったが実装部より前で例外
CSRF フィルタに引っかかってないかチェック。
Java 8 Date and Time を JSON にシリアライズするとき例外
jackson JSR-310 を入れれば良い。
pom.xml に以下を追加
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
バリデーションエラー時、 input type="password" の項目の値が空になる
セキュリティ的にあえて復元しないようになっているのかもしれない。
回避するには、とりあえず、 input type="text" としておいて、JS で切替。
$(document).ready(function () {
$("#password").attr("type", "password");
});
form をバインディングする際の検証エラーで処理が中断される
BindingResult を書いたつもりなのに、そこまで処理が来ない。
理由は簡単、BindingResult は @Valid なフォームの次に書かないと無効。
// これはダメ
@RequestMapping
String hoge(@Valid TestForm form, Model model, BindingResult result) {}
// これはOK
@RequestMapping
String hoge(@Valid TestForm form, BindingResult result, Model model) {}
デフォルトでは全ての Bean がシングルトン
Controller も、Service もシングルトン。
と言うことは、インスタンス変数に状態を保存すると他のリクエストの情報と混ざる可能性がある。
Service に @Scope("prototype") を指定すれば良いかというとそうではない。
prototype は要求されたら新しいインスタンスを生成して返す。という動作だが、そもそも
Controller が Signleton なので、最初に生成されたインスタンスがずっと使われ続ける。
基本的には Singleton で動作しても問題ないように作るべき。
※Singleton で動いても問題ない(=スレッドセーフ)に作る為にはどうすれば良いのか?
※ご参考:http://qiita.com/yoshi-naoyuki/items/507c5c3ea6027033f4bb
HttpSession に関しては特段の配慮がされているので、フィールドに入れて@Autowired しても OK
ファイルアップロードしようとしたらファイルが入ってこない
上記 URL のように正しく書いたつもりが、Form の MultiFile が null になる。
原因は簡単、 form タグに enctype="multipart/form-data" を書き忘れるとこうなる。
ログを見ると、 String から MultiFile にコンバートできなかった的なエラーが出ている。
Thymeleaf で input type="hidden"に値をセットしようとしたら入らない
th:field を使うと、form 内容と HTML 上のフォームの値が紐付いてしまう。
th:value で値を入れたい場合は、th:field ではなく、普通に name="name"で名前を合わせる。
<!-- これはダメ -->
<input type="hidden" th:field="*{somefield}" th:value="${bean.value}" />
<!-- これならOK -->
<input type="hidden" name="somefield" th:value="${bean.value}" />
ビューを表示するたびに Velocity がエラーを吐く
hoge.vm が存在しません的なもの。Velocity のオートコンフィグをしないように
しないといけないらしい。
<code class="language-java">@SpringBootApplication
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class,VelocityAutoConfiguration.class })
logger 出力をフォーマットしたい
logger.debugFormat("value of A is {0}",A); みたいなの。
slf4j を使っているなら、次の通り。
<code class="language-java">logger.debugFormat("value of A is {}",A);
application.yml にゼロ埋めの数字を書くと 8 進数扱いされる
そのまんま。
testVal: 0030
なんて書いたりすると、その値が 8 進数扱いされてしまう。文字列あたりにするのが良い。
testVal: '0030'
jar にパッケージングした時だけ TemplateError が発生する
Thymeleaf のテンプレートのパスを指定する際に、 / から始めるとそうなる。
これは、 th:include
や th:replace
のパスについても同様。
SpringBoot 1.5.x でも修正されていない。(今後も修正されない模様)
何がタチ悪いかというと、IDE 上で動かしているときはファイルシステムからテンプレートを
読むため、普通に動いてしまう。 jar にパッケージングした際に突如に動かなくなる。
devtools を使う(1.3.0 以降)
pom.xml に以下を追記
詳しくは、http://qiita.com/IsaoTakahashi/items/f99d5f761d1d4190860d
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
ただし、LiveReload 機能と Spring-Loaded を一緒に使っても、LiveReload が動いてしまうので
排他利用になりそう。個人的には Spring-Loaded の方が好き。
でも、他にも便利機能があるので dev-tools の方がよいかなとは思った。
Request method 'POST' not supported
@RequestMapping がちゃんとあるか確認するのが第一。
ちゃんとあるのであれば、CSRF フィルタに引っかかっている可能性が大。
form を submit したのに CSRF フィルタにひっかかった
th:action がないと、CSRF トークンが埋め込まれない。Javascript で動的に変えるにしても
ダミーの URL を書いておく必要がある。
<form th:object="${HogeForm}">これはダメパターン</form>
<form th:action="@{'/dummy'} th:object="${HogeForm}">
こうすればOK。トークンが自動で埋め込まれる。
</form>
Discussion