Micronaut ことはじめ - Error Handling で異常ケースも良い感じに (3)

f:id:nemuzuka:20200611200142p:plain

前回は認証機能を組み込んでみました。今回はエラーハンドリングを組み込んでみましょう。

コードはこちら

認証でエラー発生時

ログイン画面で認証失敗時、Micronaut の Session Authentication の仕組みに乗っかると、ステータスコード 401 を返します。 そのままブラウザに出すと、ちょっとかっこ悪い。

f:id:nemuzuka:20200612101949p:plain

良い感じにハンドリングしましょう。

参考にしたのは、こちら

Global Error Handling

Micronaut には、レスポンスする Http ステータスコードに応じて処理を差し込む仕組みがあります。コードは以下のような感じです。

@Slf4j
@RequiredArgsConstructor
@Controller("/errors")
public class GlobalErrorHandler {
  @Error(status = HttpStatus.UNAUTHORIZED, global = true) // 1
  public HttpResponse<?> unauthorized() {
    return HttpResponse.redirect(UriBuilder.of("/login").build()); // 2
  }
}

この設定は以下を示します。

  1. UNAUTHORIZED の時に動作します
  2. 今回は /login にリダイレクトするようにしました
    • メソッドの引数に HttpRequest を指定することもできるので、入力値を含めて画面をレンダリングすることも可能です

Micronaut を起動してログインに失敗する値を設定すると、ログイン画面が表示されるはずです。

また、ログイン後に存在しない URL を指定した時は Micronaut ステータスコード 404 を返します。そのままブラウザに出すのもちょっとあれなので画面を返すようにします。先ほどの GlobalErrorHandler に追加します。

private final ViewsRenderer viewsRenderer; // Injection 対象

@Error(status = HttpStatus.NOT_FOUND, global = true)
public HttpResponse<?> notFoundForPage() {
  return HttpResponse.ok(viewsRenderer.render("notFound", Collections.EMPTY_MAP))
      .contentType(MediaType.TEXT_HTML);
}

ViewsRenderer がでてきました。ViewsRenderer#render を呼び出すことでテンプレートエンジンを元に html をレスポンスすることができます。

src/main/resources/views/notFound.html を配置して Micronaut を起動してログイン成功した後に存在しない URL を叩いた時(e.g. http://localhost:8080/hoge`)、notFound.html の画面が表示されるはずです。

f:id:nemuzuka:20200612104114p:plain

サーバサイドの validation

ユーザビリティのために js で入力値をチェックするかもしれませんが、サーバサイドでも validation した方が安心できます。Bean Validationを元にやってみましょう。

http://localhost:8080/tasks/add で表示される画面です。

f:id:nemuzuka:20200612105158p:plain

Form

入力パラメータを受け取る Form はこんな感じ。*1

@Data
@Introspected
public class TaskForm {

  /** task_name. */
  @NotNull
  @Size(min = 1, max = 256)
  private String taskName;

  /** content. */
  @NotNull
  @Size(min = 1, max = 1024)
  private String content;
}

@Introspected が必要になります。@NotNull@Size が付いてます。これが validation のルールになります。Spring っぽいですね。

Controller

登録処理を行う Controller はこんな感じ。

@Controller("/tasks/add")
@RequiredArgsConstructor
public class AddController {
  @Post
  @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
  public HttpResponse<String> addTask(@Body @Valid TaskForm taskForm) {
    var uri = UriBuilder.of("/tasks").build();
    return HttpResponse.redirect(uri);
  }
}

見やすさのためにひとまず登録処理自体は省いてます。 @Body で body パラメータをマッピング@Valid で validation を行うようになります。

Local Error Handling

Controller のメソッドが呼ばれる前に validation を行うのですが、不正な入力があると ConstraintViolationException を throw するのでそれをハンドリングする処理が必要になります。

AddController 内にメソッド追加します。

@View("tasks/edit") // 1
@Error(exception = ConstraintViolationException.class) // 2
public Map<String, Object> onFailed(
    HttpRequest<Map<String, Object>> request, ConstraintViolationException ex) {
  Map<String, Object> responseMap = new HashMap<>();
  responseMap.put(
      "errors",
      constraintViolationMessageConverter.violationsMessages(ex.getConstraintViolations())); // 3
  request.getBody(TaskForm.class).ifPresent(form -> responseMap.put("taskForm", form)); // 4
  return responseMap;
}
  1. この Error Handling でレスポンスする view を指定します
  2. ConstraintViolationException を catch したら動作します
  3. view をレンダリングする時に参照するデータとして、エラーメッセージを設定します。ConstraintViolationException から情報を生成する為に自前で ConstraintViolationMessageConverter を作成しています。詳細はコードを見てみてください
  4. view をレンダリングする時に参照するデータとして form 情報を設定します。

このようにしておくことで、validation エラーが発生した時に再度入力画面を表示するようになります。

Micronaut を起動してログイン成功した後に http://localhost:8080/tasks にアクセスして、項目を未入力で登録すると validation のエラーが表示されるようになるはずです。

f:id:nemuzuka:20200612113622p:plain

SpringBoot の方式とは違いますが(BindingResult とかではない)、Exception を throw してハンドリングする方式もメソッドの見通しはよくなるのでこれはこれでアリかな、と思います。

Thymeleaf のタグ

SpringBoot を使っていると普通に使ってた th:field="*{title}" とかの記法ですが、Micronaut では使えず、th:value="*{title}" で設定しないといけません。input タグの name や id も自分で設定します。

textarea は th:value ではなく、

<textarea th:utext="*{content}"></textarea>

のように th:utext を使用します。

このあたりは Spring の拡張機能のありがたみを感じられるところでしょうか。

まとめ

今回は、グローバルなエラーハンドリングと Controller 毎のローカルなエラーハンドリングについて書きました。ちょっと面倒に思うかもしれませんが、アノテーションで簡単な validation ができるのは楽ができて良いと思います。

*1:struts っぽいですけど Form で通じますよね...?