🌊

SpringBoot(1.2くらい)を使っていてハマった事

7 min read

はじめに

Spring Boot 1.2.5 を使いはじめてハマった事をメモします。

既にご存じかとは思いますが、以下の Qiita がとても良くまとまっているので

ご紹介します。

#1.2.8 でも同じようにハマれるのを確認しています

SpringBoot(with Thymeleaf)チートシート[随時更新]

http://qiita.com/uzresk/items/31a4585f7828c4a9334f

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

ファイルアップロードしようとしたらファイルが入ってこない

http://blog.okazuki.jp/entry/2015/07/17/202941

上記 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);

http://www.slf4j.org/faq.html#logging_performance

application.yml にゼロ埋めの数字を書くと 8 進数扱いされる

そのまんま。

testVal: 0030

なんて書いたりすると、その値が 8 進数扱いされてしまう。文字列あたりにするのが良い。

testVal: '0030'

jar にパッケージングした時だけ TemplateError が発生する

Thymeleaf のテンプレートのパスを指定する際に、 / から始めるとそうなる。

これは、 th:includeth:replace のパスについても同様。

SpringBoot 1.5.x でも修正されていない。(今後も修正されない模様)

https://github.com/spring-projects/spring-boot/issues/1744

何がタチ悪いかというと、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

ログインするとコメントできます