読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + Thymeleaf の Web アプリを MockMvc でテストした時に遅かった理由とは?

Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その6 )( 検索/一覧画面 ( MyBatis-Spring版 ) 作成 ).andExpect(content().contentType("text/html;charset=UTF-8")) 以降を書くと時間がかかるようになると書きましたが、原因が分かりました。以下に調査した内容と、結果を記載します。

まず調査前に発生していた問題の内容は、.andExpect(...) 以降の部分を記述するとテストに時間がかかる ( 1テストに 1分くらいかかる )、ということでした。

下記の最初のテストの場合 1分程度かかり、次のステータスしかチェックしていないテストではすぐに終わります。

    @Test
    public void testIndex() throws Exception {
        this.mvc.perform(get("/countryList")).andExpect(status().isOk())
                .andExpect(content().contentType("text/html;charset=UTF-8"))
                .andExpect(view().name("countryList"))
                .andExpect(xpath("/html/head/title").string("検索/一覧画面"));
    }
    @Test
    public void testIndex() throws Exception {
        this.mvc.perform(get("/countryList")).andExpect(status().isOk());
    }

MockMvc の戻り値からコンテンツを String で取得する方法があることが分かったので、以下のソースに変えてみました。XPath は使わず、単に文字列が含まれているかのチェックのみしています。これもすぐに終わりました。

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertThat;

.....

    @Test
    public void testIndex() throws Exception {
        MvcResult result = this.mvc.perform(get("/countryList")).andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        assertThat(content, containsString("<title>検索/一覧画面</title>"));
    }

次に XPath でチェックしてみます。

Spring Frameworkxpath のテストで利用されていると思われる XpathExpectationsHelper.java のソースを見てみると、 org.w3c.dom.Document クラスのインスタンスを生成してテストしているようでしたので、同じように org.w3c.dom.Document クラスのインスタンスを生成し JUnit4 のチェックで使用されている org.hamcrest の Matchers クラスを利用してチェックしてみます。これは最初に記載したテストと同じく 1分程度時間がかかりました。

import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.hasXPath;

.....

    @Test
    public void testIndex() throws Exception {
        MvcResult result = this.mvc.perform(get("/countryList")).andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        Document document = DocumentBuilderFactory
                .newInstance()
                .newDocumentBuilder()
                .parse(new InputSource(new StringReader(content)));
        assertThat(document, hasXPath("/html/head/title", equalTo("検索/一覧画面")));
    }

チェックで時間がかかるとは思えないので org.w3c.dom.Document クラスのインスタンスを作っているところで時間がかかっているのだろうと思い、前後に System.out.println でログを出力してみると確かにここで時間がかかっていることが判明。

        System.out.println("★★★ Start");
        Document document = DocumentBuilderFactory
                .newInstance()
                .newDocumentBuilder()
                .parse(new InputSource(new StringReader(content)));
        System.out.println("★★★ End");
2015-01-25 13:51:24.164 DEBUG 3732 --- [    Test worker] o.s.w.s.v.ContentNegotiatingViewResolver : Returning [org.thymeleaf.spring4.view.ThymeleafView@3fcfcea] based on requested media type '*/*'
★★★ Start
★★★ End
2015-01-25 13:52:25.451  INFO 3732 --- [    Test worker] o.s.t.c.web.WebTestContextBootstrapper   : Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]

Thymeleaf も同じように HTML を解析して処理している ( HTML内で / を付け忘れたら org.xml.sax.SAXParseException を見たことがあった ) が処理速度は全然問題がないことから、同じように HTML を解析している処理で時間がかかる訳がないはずと思い、いろいろ調べていると以下の記事を発見しました。

AndroidでのXML解析と、Evernote本文のDocumentパースが激重い話

遅い原因となっている部分を取り除くと早くなる模様。そこで削除すると早くなりそうなところがないか確認するために HTMLを見ると先頭に以下のソースが。

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>

外部のリソースが記述されているのでこの辺かなと思い、以下の検証を実施しました。

まずは <!DOCTYPE ...><!DOCTYPE html> に変更してみたところ、速くなることが判明!

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>

次に DOCTYPE はそのままで <html ...><html> に変更してみたところ、これは 1分かかる状況が変わりませんでした。

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html>
<head>

最後に <!DOCTYPE ...><!DOCTYPE html><html ...><html> のどちらも変更もしてみた場合も試すと、これも速くなりました。

<!DOCTYPE html>
<html>
<head>

結論として、原因は <!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd"> の記述でした。

ただし Thymeleaf の Document の「13.2 Doctype translation」 には記載があるし、これを入れないと IntelliJ IDEA 14 で実装された Thymeleaf のサポートが動作しないらしい。

IntelliJ IDEA 14 の Thymeleaf サポートは試してみたら Community Edition では 動作しないようなので、他の解決策が見つかるまでは <!DOCTYPE html> に変えることにします。