LoginSignup
9
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-28

この記事の概要

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

環境

  • Java8
  • spring-boot-starter-web:2.1.1
  • spring-boot-starter-thymeleaf:2.1.1

調査

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

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

management:
  endpoints:
    web:
      exposure:
        include: "heapdump"
      base-path: "/"
  server:
    port: 9990

上記のようにincludeheapdumpを記載することで以下のように実行するとheampdumpが取得できる!

curl localhost:9990/heapdump -o heap.dump

あとはEclipseのMemoryAnalyzerで調査!

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

原因

  • MemoryAnalyzerで調査したところリダイレクト時にURLをデフォルトでキャッシュしている処理があり、キャッシュが溢れているようだった
  • こんなのがダメ
リダイレクトサンプル
@Controller
public class Sample {
    @GetMapping("/sample")
    public String sample() {
        return "redirect:/hoge/" + UUID.randomUUID().toString();
    }
}
  • org.thymeleaf.spring5.view.ThymeleafViewResolver.createViewが犯人
抜粋
// 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を継承して以下のようなクラスを作った
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化しているところを消しただけ
  • 解決!!!
9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7