SpringBoot(Thymeleaf)で動的なリダイレクトをするとメモリリークする

# この記事の概要
- SpringBootを使用したWebアプリケーションを運用していた
- JVMメモリを見てみると単調増加している。。。
- 調査・対応してみたのでそのときのメモ

# 環境
- Java8
- spring-boot-starter-web:2.1.1
- spring-boot-starter-thymeleaf:2.1.1

# 調査
- まずはどんなオブジェクトが増え続けているのか調べる
- アプリケーションはkubernetesで管理されている
- 権限管理の問題でpod内には入れない　→　jmapでheapdumpが取れない:scream:

そこでmanagementエンドポイントを使うことにした

```yaml
management:
  endpoints:
    web:
      exposure:
        include: "heapdump"
      base-path: "/"
  server:
    port: 9990
```
上記のように`include`に`heapdump`を記載することで以下のように実行するとheampdumpが取得できる！

```bash
curl localhost:9990/heapdump -o heap.dump
```

あとはEclipseのMemoryAnalyzerで調査！

> 使い方や調査結果は省略させていただきます:bow_tone1:

# 原因
- MemoryAnalyzerで調査したところリダイレクト時にURLをデフォルトでキャッシュしている処理があり、キャッシュが溢れているようだった
- こんなのがダメ

```java:リダイレクトサンプル
@Controller
public class Sample {
    @GetMapping("/sample")
    public String sample() {
        return "redirect:/hoge/" + UUID.randomUUID().toString();
    }
}
```

- `org.thymeleaf.spring5.view.ThymeleafViewResolver.createView`が犯人

```java:抜粋
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
    vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
    final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length());
    final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
    return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
```
- リダイレクトURLごとにBean生成しているのね。。。

# 対応

- ThymeleafViewResolverを継承して以下のようなクラスを作った

```java
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.RedirectView;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

@Slf4j
public class ThymeleafViewResolverWrapper extends ThymeleafViewResolver {

    public static final String REDIRECT_URL_PREFIX = "redirect:";

    @Override
    protected View createView(final String viewName, final Locale locale) throws Exception {
        // First possible call to check "viewNames": before processing redirects and forwards
        if (!getAlwaysProcessRedirectAndForward() && !canHandle(viewName, locale)) {
            log.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        }
        // Process redirects (HTTP redirects)
        if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
            log.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
            final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
            return new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
        }
        // Process forwards (to JSP resources)
        if (viewName.startsWith(FORWARD_URL_PREFIX)) {
            // The "forward:" prefix will actually create a Servlet/JSP view, and that's precisely its aim per the Spring
            // documentation. See http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/mvc.html#mvc-redirecting-forward-prefix
            log.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
            final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
            return new InternalResourceView(forwardUrl);
        }
        // Second possible call to check "viewNames": after processing redirects and forwards
        if (getAlwaysProcessRedirectAndForward() && !canHandle(viewName, locale)) {
            log.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        }
        log.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a " +
                "{} instance will be created for it", viewName, getViewClass().getSimpleName());
        return loadView(viewName, locale);
    }
}
```

- Bean化しているところを消しただけ
- 解決！！！