Spring Web Reactive | 1. Spring WebFlux | 1.4. Annotated Controllers #2

1.4.4. Model

Web MVC

@ModelAttributeアノテーションを使用できます。

モデル内のオブジェクトを作成または参照し、WebDataBinderを通じてリクエストへバインドする@RequestMappingメソッドのメソッド引数として使用できます。

@Controllerまたは@ControllerAdviceクラスのメソッドレベルアノテーションとして、@RequestMappingメソッドを呼び出す前にモデルを初期化できます。

@RequestMappingメソッドの戻り値をモデル属性として公開することもできます。

このセクションでは、@ModelAttributeメソッド、つまり前のリストの2番目の項目について説明します。コントローラーはいくつでも@ModelAttributeメソッドを持つことができます。これらのメソッドはすべて、同じコントローラー内の@RequestMappingメソッドより前に呼び出されます。@ModelAttributeメソッドは、@ControllerAdviceを通じて複数のコントローラー間で共有することもできます。詳しくはコントローラーアドバイスのセクションを参照してください。

@ModelAttributeメソッドは柔軟なメソッドシグネチャを持ちます。@ModelAttribute自体とリクエスト本文に関連するものを除き、@RequestMappingメソッドと同じ多くの引数をサポートします。

次の例では、@ModelAttributeメソッドを使用しています。

Java

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}

Kotlin

@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}

次の例では、1つの属性だけを追加します。

Java

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}

Kotlin

@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}

名前を明示的に指定しない場合、ConventionsのJavadocに従って型に基づくデフォルト名が選択されます。オーバーロードされたaddAttributeメソッドを使用するか、戻り値に対して@ModelAttributename属性を使用すると、いつでも明示的な名前を指定できます。

Spring WebFluxはSpring MVCと異なり、モデル内のリアクティブ型を明示的にサポートします。例えば、Mono<Account>io.reactivex.Single<Account>などです。次の例のように、@ModelAttribute引数がラッパーなしで宣言されている場合、そのような非同期モデル属性は@RequestMapping呼び出し時に実際の値へ透過的に解決され、モデルも更新されます。

Java

@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}

Kotlin

import org.springframework.ui.set

@ModelAttribute
fun addAccount(@RequestParam number: String) {
    val accountMono: Mono<Account> = accountRepository.findAccount(number)
    model["account"] = accountMono
}

@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
    // ...
}

リアクティブ型ラッパーを持つモデル属性も、ビューのレンダリング直前に実際の値へ解決され、モデルが更新されます。

@ModelAttributeは、@RequestMappingメソッドのメソッドレベルアノテーションとして使用することもできます。この場合、@RequestMappingメソッドの戻り値はモデル属性として解釈されます。これはHTMLコントローラーのデフォルト動作であり、戻り値がビュー名として解釈されるStringの場合を除いて、通常は必要ありません。@ModelAttributeは次の例のように、モデル属性名をカスタマイズする場合にも役立ちます。

Java

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}

Kotlin

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}

1.4.5. DataBinder

Web MVC

@Controllerまたは@ControllerAdviceクラスは、WebDataBinderインスタンスを初期化するために@InitBinderメソッドを持つことができます。これらは次の目的で使用されます。

リクエストパラメータ、つまりフォームデータやクエリをモデルオブジェクトへバインドします。

Stringベースのリクエスト値、例えばリクエストパラメータ、パス変数、ヘッダー、Cookieなどを、コントローラーメソッド引数の対象型へ変換します。

HTMLフォームをレンダリングするときに、モデルオブジェクトの値をString値としてフォーマットします。

@InitBinderメソッドは、コントローラー固有のjava.beans.PropertyEditor、またはSpringのConverterFormatterコンポーネントを登録できます。また、WebFlux Java設定を使用して、ConverterFormatter型をグローバルに共有されるFormattingConversionServiceへ登録することもできます。

@InitBinderメソッドは、@ModelAttribute、つまりコマンドオブジェクト引数を除き、@RequestMappingメソッドと同じ多くの引数をサポートします。通常はWebDataBinder引数とvoid戻り値で宣言されます。次の例では、@InitBinderアノテーションを使用しています。

Java

@Controller
public class FormController {

    @InitBinder    // (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}

Kotlin

@Controller
class FormController {

    @InitBinder   // (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}
  • (1) @InitBinderアノテーションを使用します。

また、共有FormattingConversionServiceを通じてFormatterベースの設定を使用する場合は、同じアプローチを再利用して、次の例のようにコントローラー固有のFormatterインスタンスを登録できます。

Java

@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));  // (1)
    }

    // ...
}

Kotlin

@Controller
class FormController {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))  // (1)
    }

    // ...
}
  • (1) カスタムフォーマッター、この場合はDateFormatterを追加します。

1.4.6. 例外管理

Web MVC

@Controller@ControllerAdviceクラスは、コントローラーメソッドからの例外を処理する@ExceptionHandlerメソッドを含めることができます。次の例には、そのようなハンドラーメソッドが含まれています。

Java

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler  // (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

Kotlin

@Controller
class SimpleController {

    // ...

    @ExceptionHandler // (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}
  • (1) @ExceptionHandlerを宣言します。

例外は、伝播される最上位の例外、つまり直接発生したIOException、または最上位のラッパー例外の直接原因、例えばIllegalStateExceptionにラップされたIOExceptionと一致します。

例外の型を一致させるには、前の例のように対象の例外をメソッド引数として宣言することを推奨します。または、アノテーション宣言で一致する例外型を制限できます。一般には、引数シグネチャをできるだけ具体的にし、その順序で優先順位付けされた@ControllerAdviceに一般的なルート例外マッピングを宣言することをおすすめします。詳しくはMVCセクションを参照してください。

WebFluxの@ExceptionHandlerメソッドは、@RequestMappingメソッドと同じメソッド引数と戻り値をサポートします。ただし、リクエスト本文および@ModelAttribute関連のメソッド引数は例外です。 SpringのWebFluxにおける@ExceptionHandler方式のサポートは、@RequestMappingメソッドのHandlerAdapterによって提供されます。詳しくはDispatcherHandlerを参照してください。

REST APIの例外

Web MVC

RESTサービスで一般的に必要になるのは、レスポンス本文にエラー情報を含めることです。Spring Frameworkは、レスポンス本文におけるエラー詳細の表現がアプリケーション固有であるため、自動では行いません。ただし、@RestControllerResponseEntity戻り値を持つ@ExceptionHandlerメソッドを使用して、レスポンスのステータスと本文を設定できます。このようなメソッドを@ControllerAdviceクラスで宣言すると、グローバルに適用できます。

Spring WebFluxには、Spring MVCのResponseEntityExceptionHandlerに相当するものがない点に注意してください。これは、WebFluxがResponseStatusException、またはそのサブクラスだけを生成し、HTTPステータスコードへ変換すればよいためです。

1.4.7. コントローラーアドバイス

Web MVC

通常、@ExceptionHandler@InitBinder@ModelAttributeメソッドは、それらが宣言された@Controllerクラス、またはクラス階層に適用されます。同じメソッドをコントローラー全体へよりグローバルに適用したい場合は、@ControllerAdviceまたは@RestControllerAdviceアノテーションを付けたクラスで宣言できます。

@ControllerAdviceには@Componentアノテーションが宣言されています。これは、このようなクラスをコンポーネントスキャンによってSpring Beanとして登録できることを意味します。@RestControllerAdviceは、@ControllerAdvice@ResponseBodyの両方が付いた合成アノテーションです。これは本質的に、@ExceptionHandlerメソッドがビューリゾルバー処理やテンプレートレンダリングではなく、メッセージ変換によってレスポンス本文へレンダリングされることを意味します。

起動時に、@RequestMapping@ExceptionHandlerメソッドのインフラクラスは、@ControllerAdviceアノテーションが宣言されたSpring Beanを検出し、実行時にそのメソッドを適用します。グローバルな@ExceptionHandlerメソッド、つまり@ControllerAdvice内のメソッドは、ローカルメソッド、つまり@Controller内のメソッドの後に適用されます。対照的に、グローバルな@ModelAttributeおよび@InitBinderメソッドは、ローカルメソッドの前に適用されます。

デフォルトでは、@ControllerAdviceメソッドはすべてのリクエスト、つまりすべてのコントローラーに適用されます。ただし、次の例のようにアノテーション属性を使用して、コントローラーのサブセットへ範囲を絞ることができます。

Java

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

Kotlin

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}

前の例のセレクターは実行時に評価され、広範囲に使用するとパフォーマンスへ悪影響を与える可能性があります。詳しくは@ControllerAdviceを参照してください。