Spring WebfluxとThymeleafの連携がどのように行われているのか

きっかけは次のコード。

```kotlin
@Controller
@RequestMapping("/plugins")
class PluginController {
  /**
   * 設定ファイルはアクセス禁止。
   */
  @RequestMapping("/{pluginId}/plugin.conf")
  fun preventAccess(): Mono<ResponseEntity<Any>> {
    return Mono.just(ResponseEntity.notFound().build<Any>())
  }
  /**
   * 他のhtmlファイルはアクセス許可。
   */
  @GetMapping("/{pluginId}/**")
  fun view(@PathVariable pluginId: String): Mono<Any> {
    val viewId = /* pluginIdに該当する設定ファイルより、パスに応じたビューIDを取得 */
    return (null == viewId) ?
        // ビューがない
        Mono.just(ResponseEntity.notFound().build<Any>()) :
        // ビューを表示
        Mono.just("$pluginId/$viewId")
  }
```
UIとして生のHTTPステータスを返すのは如何なものか？という、そもそも論は一旦置いといて、

* preventAccess()でMono<ResponseEntity<Any>>を返却すると、ブラウザに「404 Not Found」が表示される。
* view()でMono<String>を返却すれば、Thymeleafを介してブラウザにページが表示される。

と期待通りに動くのですが、

* view()が呼び出され、Mono<ResponseEntity<Any>>を返却すると、Thymeleafで「org.thymeleaf.exceptions.TemplateInputException: Error resolving template」が発生してしまう。

となります。おそらくメソッドから実際に返却されたオブジェクトの型ではなく、メソッドで宣言された戻り値の型でHTTPステータスが返却されるか、Thymeleafが処理するかが変わるのだと推測されます。

しかし、具体的にはどのような処理なのか確認しようと思っても

```
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [plugins/hoge], template might not exist or might not be accessible by any of the configured Template Resolvers
	at org.thymeleaf.engine.TemplateManager.resolveTemplate(TemplateManager.java:869) ~[thymeleaf-3.0.11.RELEASE.jar:3.0.11.RELEASE]
	at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:607) ~[thymeleaf-3.0.11.RELEASE.jar:3.0.11.RELEASE]
	at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) ~[thymeleaf-3.0.11.RELEASE.jar:3.0.11.RELEASE]
	at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072) ~[thymeleaf-3.0.11.RELEASE.jar:3.0.11.RELEASE]
	at org.thymeleaf.spring5.SpringWebFluxTemplateEngine.lambda$createFullStream$0(SpringWebFluxTemplateEngine.java:212) ~[thymeleaf-spring5-3.0.11.RELEASE.jar:3.0.11.RELEASE]
	at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:57) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoLog.subscribe(MonoLog.java:51) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1510) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:147) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.java:60) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:150) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
	at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1510) ~[reactor-core-3.2.9.RELEASE.jar:3.2.9.RELEASE]
```

と、ひたすらreactorのスタックトレースが続いてて、何がどうなってるやら。リアクティブストリームって自分の理解だと
![](http://www.plantuml.com/plantuml/png/RSinJeH05CRn_ftYLNRi0GmXoy1m2Xk4Xs86CCEZi5GiJFGEQgl9TXOctcPYrcCuCs5T0qmr_r_Uj-qvrpmq2Yxhaf2QQxx1gYPLzhX60Dtv5HMCCOQXwYxg8eeE99QKB2crrBEcl3cQNB7ilq6CGOJY58jSAH0kaYaWTIMdaYwoAUngiuamjQafPmBXM_vruZixj1k4GU06Rd_MdcZYGRSWFCiv9vwpETlNTaoeg6BiAep1t7sQyS6CRsRyCkEBkTtjF-xtxuz0ROdkThP8LnRVk-UVrwVZOcj_GwD-0G00)
こんなイメージなので、ストリームを組み立てたロジックを調べないとダメなんだろうな。

ってことで、ちょっと腰を据えて調べてみた。対象はSpring Bootのバージョン2.1.5.RELEASE。

# 調査
## SpringWebFluxTemplateEngine
まずはストリームから呼び出された(上記イメージのCにあたる)SpringWebFluxTemplateEngineから呼び出し元を辿ってみた。SpringWebFluxTemplateEngineは、ConditionalOnWebApplicationアノテーションによりアプリケーションがリアクティブならBeanとして登録されている。

```java:spring-boot-autoconfigure-2.1.5.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java
@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled",matchIfMissing = true)
static class ThymeleafReactiveConfiguration {
  ////// 省略 //////
  @Bean
  @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
  public SpringWebFluxTemplateEngine templateEngine() {
    SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
    ////// 省略 //////
    return engine;
  }
}
```

## ThymeleafReactiveViewResolver
ISpringWebFluxTemplateEngineインタフェースを実装しているSpringWebFluxTemplateEngineは、ThymeleafReactiveViewResolverに渡されている。

```java:spring-boot-autoconfigure-2.1.5.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.j@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebFluxConfiguration {
  ////// 省略 //////
	@Bean
	@ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
	public ThymeleafReactiveViewResolver thymeleafViewResolver(
			ISpringWebFluxTemplateEngine templateEngine) {
		ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
		resolver.setTemplateEngine(templateEngine);
    ////// 省略 //////
		return resolver;
	}
  ////// 省略 //////
}
```
ThymeleafReactiveViewResolverは、Spring Webfluxとインタフェース境界となるViewResolverを実装しており、resolveViewNameメソッドが呼び出されるとThymeleafReactiveViewを生成して返却している。

![](http://www.plantuml.com/plantuml/png/XP1DJiCm48NtFiKiOP4U05NP85Yo21Mi9yvBOiK_rjXfgA1t9usg8A4IbkFvdlo-IOFnaOKTrQo7JiBecyTWWz3KcupamW3LcmO45_E975mDKKfRJeaE_Sd1WjiAhBCvuirWsjfLIC4U8LVcDOhntJlgPpj-7E2YvOmdtnaFfU9SY3jGMg1YGjtE5Ett5NHctrcK9Nqg8kCpf6KDkdonF149V0Evkv-7yszchR6ootZPlB33DNHMwgO35SLNIOlffkFA_LZ_4zZjIhf9F90MS8Rwgr8encBhcjy0)

そこでThymeleafReactiveViewResolver#resolveViewName()にブレークポイントを指定して、IDEでデバッグしてみると…

## ViewResolutionResultHandler
ViewResolutionResultHandler#handleResultからの流れで生成されてストリーム中(★⇒★★)で呼び出されていたの。

```java:spring-webflux-5.1.7.RELEASE-sources.jar!/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java
  @Override
  public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
     /// 省略 ///
    return valueMono
        .switchIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO)
        .flatMap(returnValue -> {
          Model model = result.getModel();
          Mono<List<View>> viewsMono = /// さまざまな条件で★resolveViewsを呼び出している ///
          return viewsMono.flatMap(views -> render(views, model.asMap(), exchange));
        });
  }

  private Mono<List<View>> resolveViews(String viewName, Locale locale) {
    return Flux.fromIterable(getViewResolvers())
        .concatMap(resolver -> resolver./*★★*/resolveViewName(viewName, locale))
        /// 省略 ///);
  }
```
今度はhandleResultメソッドが呼び出しされる時点にブレークさせてみると…

## DispatcherHandler
DispatcherHandler#handleメソッドで生成されたストリーム中(★⇒★★)から呼び出されてました。

```java:spring-webflux-5.1.7.RELEASE-sources.jar!/org/springframework/web/reactive/DispatcherHandler.java
  @Override
  public Mono<Void> handle(ServerWebExchange exchange) {
    if (this.handlerMappings == null) {
      return createNotFoundError();
    }
    return Flux.fromIterable(this.handlerMappings)
        .concatMap(mapping -> mapping.getHandler(exchange))
        .next()
        .switchIfEmpty(createNotFoundError())
        .flatMap(handler -> invokeHandler(exchange, handler))
        .flatMap(result -> /*★*/handleResult(exchange, result));
  }
  private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
    return getResultHandler(result)./*★★*/handleResult(exchange, result)
        /// 省略 ///;
  }
  private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {
    if (this.resultHandlers != null) {
      for (HandlerResultHandler resultHandler : this.resultHandlers) {
        if (resultHandler.supports(handlerResult)) {
          return resultHandler;
    /// 省略 ///;
  }
```

Dispatcher〜と言えば、Spring MVCでもDispatcherServletが処理の起点となっていたので、DispatcherHandlerも同様の位置づけと思われます。

今回、DispatcherHandler.resultHandlersには、

* ResponseEntityResultHandler
* ServerResponseResultHandler
* ResponseBodyResultHandler
* ViewResolutionResultHandler

の順にHandlerResultHandlerインタフェースを実装したオブジェクトが格納され、コントローラでMono<ResponseEntity<Any>>を返却した場合にはResponseEntityResultHandlerが、Mono<String>を返却した場合にはViewResolutionResultHandlerが処理しているようです。

## ResponseEntityResultHandler
今回の疑問はMono<ResponseEntity<Any>>を返却しているのに処理しないResponseEntityResultHandler#supports()に答えがあるようです。

```java:spring-webflux-5.1.7.RELEASE-sources.jar!/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandler.java

  @Override
  public boolean supports(HandlerResult result) {
    Class<?> valueType = resolveReturnValueType(result);
    if (/*★*/isSupportedType(valueType)) {
      return true;
    }
    ReactiveAdapter adapter = getAdapter(result);
    return adapter != null && !adapter.isNoValue() &&
        /*★★*/isSupportedType(result.getReturnType().getGeneric().toClass());
  }
```
resolveReturnValueTypeはハンドラ(今回はコントローラのメソッド)の戻り値のクラスを返す。

isSupportedTypeは指定されたクラスが、ResponseEntityを継承している(実装上はHttpEntityを継承して、かつRequestEntityを継承していない場合)か、HttpHeadersを継承している場合にtrueを返す。
よってMono<ResponseEntity<Any>>は、★でfalseになるが、★★で型引数を判定するのでtrueになるが、
Mono<Any>はどちらもfalseとなりResponseEntityResultHandlerでは処理されないということでした。