かんがるーさんの日記

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

Spring Boot 2.1.x の Web アプリを 2.2.x へバージョンアップする ( その13 )( gradle の VM オプションを gradle.proeprties に移行する+spring.main.lazy-initialization を試してみる2 )

概要

記事一覧はこちらです。

Spring Boot 2.1.x の Web アプリを 2.2.x へバージョンアップする ( その12 )( spring.main.lazy-initialization を試してみる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • IntelliJ IDEA の Gradle の VM オプションの設定項目が deprecated になっていたので、gradle.properties に移行します。
    • ユニットテスト実行時に -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加してテストの時間を短縮できるか試してみます。

参照したサイト・書籍

目次

  1. gradle の VM オプションを gradle.proeprties に移行する
  2. ユニットテスト実行時の VM オプションに -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加する

手順

gradle の VM オプションを gradle.proeprties に移行する

IntelliJ IDEA の設定を見直していた時に Gradle VM options の設定項目が deprecated になっていることに気づきました。代わりに gradle.properties に設定するよう記述されています。

f:id:ksby:20200112235406p:plain

プロジェクトのルートディレクトリ直下に gradle.properties を新規作成し、以下の内容を記述します。

org.gradle.jvmargs=-Dfile.encoding=UTF-8

IntelliJ IDEA の設定項目の値をクリアすると、設定項目自体が消えました。

f:id:ksby:20200113101735p:plain

ユニットテスト実行時の VM オプションに -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加する

Spring Boot 2.1.x の Web アプリを 2.2.x へバージョンアップする ( その12 )( spring.main.lazy-initialization を試してみる )spring.main.lazy-initialization 設定時の動作を確認しましたが、ユニットテスト実行時に追加した時の動作確認をし忘れていましたので確認してみます。

build.gradle の jvmArgsForTask 変数に -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加します。testJUnit4AndSpock、test タスクだけでなく bootRun タスク時に付いていても問題ないので、共通の VM オプションを設定している変数 jvmArgsForTask に追加します。

def jvmArgsForTask = [
        "-ea",
        "-Dfile.encoding=UTF-8",
        "-Dsun.nio.cs.map=x-windows-iso2022jp/ISO-2022-JP",
        "-XX:TieredStopAtLevel=1",
        "-Dspring.main.lazy-initialization=true"
]

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、テストが1件失敗しました。

f:id:ksby:20200113104000p:plain

失敗したのは以下のテストです。このテストだけ実行してみます。

    @Nested
    @SpringBootTest
    class 次回から自動的にログインするのテスト {

        @RegisterExtension
        @Autowired
        public TestDataExtension testDataExtension;

        @RegisterExtension
        @Autowired
        public SecurityMockMvcExtension mvc;

        @Autowired
        ServletContext servletContext;

        @Test
        void 次回から自動的にログインするをチェックすれば次はログインしていなくてもログイン後の画面にアクセスできる()
                throws Exception {
            // ログイン前にはログイン後の画面にアクセスできない
            mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("http://localhost/"))
                    .andExpect(unauthenticated());

            // 「次回から自動的にログインする」をチェックしてログインし、remember-me Cookie を生成する
            org.springframework.mock.web.MockHttpServletRequest request
                    = formLogin()
                    .user("id", mvc.MAILADDR_TANAKA_TARO)
                    .password("password", "taro")
                    .buildRequest(servletContext);
            request.addParameter("remember-me", "true");
            SimpleRequestBuilder simpleRequestBuilder = new SimpleRequestBuilder(request);
            MvcResult result = mvc.noauth.perform(simpleRequestBuilder)
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN))
                    .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO))
                    .andReturn();
            Cookie[] cookie = result.getResponse().getCookies();

            // remember-me Cookie を引き継いでログイン後の画面にアクセスするとアクセスできる
            mvc.noauth.perform(get(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN).cookie(cookie))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(model().hasNoErrors())
                    .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO));

            // ログイン画面にアクセスしても有効な remember-me Cookie があればログイン後の画面にリダイレクトする
            mvc.noauth.perform(get("/").cookie(cookie))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl(Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN))
                    .andExpect(authenticated().withUsername(mvc.MAILADDR_TANAKA_TARO));
        }

    }

IntelliJ IDEA のエディタから上のテストを1度実行した後(この時は -Dspring.main.lazy-initialization=true を指定していないので成功します)、メインメニューの「Run」-「Edit Configurations...」を選択します。

「Run/Debug Configurations」ダイアログが表示されたら、JUnit のテストの VM options に -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加します。

f:id:ksby:20200113104714p:plain

IntelliJ IDEA の画面上部から JUnit のテストを選択し実行すると、

f:id:ksby:20200113104845p:plain

今度は失敗しました。java.lang.IllegalStateException: UserDetailsService is required. のエラーメッセージが出ています。

f:id:ksby:20200113104947p:plain

LazyInitializationExcludeFilter.forBeanTypes(...) で UserDetailsService や LendingUserDetailsService を Lazy の対象から除外してみましたが、テストは成功しませんでした。

breakpoint を設定して何度も debug 実行して分かったのは以下の点でした。

  • テストが失敗するのは以下のコードが実行された時でした。 f:id:ksby:20200113105448p:plain

  • -Dspring.main.lazy-initialization=true を付けている時と付けない時で WebSecurityConfigurerAdapter$UserDetailsServiceDelegator#loadUserByUsername 内の delegate = delegateBuilder.getDefaultUserDetailsService(); の動作が異なっていました。 f:id:ksby:20200113105803p:plain

    -Dspring.main.lazy-initialization=true を付けている時は、delegateBuilder = AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder の時に delegateBuilder.getDefaultUserDetailsService() は null を返すのですが、 f:id:ksby:20200113105946p:plain

    -Dspring.main.lazy-initialization=true を付けていない時は、delegateBuilder = AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder の時に delegateBuilder.getDefaultUserDetailsService() は ksbysample.webapp.lending.security.LendingUserDetailsService のインスタンスを返します。 f:id:ksby:20200113110430p:plain

  • このアプリで defaultUserDetailsService に影響しそうなのは ksbysample.webapp.lending.config.WebSecurityConfig#configAuthentication しかなかったので breakpoint を設定してみたところ、-Dspring.main.lazy-initialization=true を付けている時には breakpoint で止まりませんでした。lazy の対象になっていて起動時に実行されず、後で必要になってからも実行されていないようです。 f:id:ksby:20200113110848p:plain

そうであれば ksbysample.webapp.lending.config.WebSecurityConfig クラス自体を lazy の対象外にすれば解決しそうに思えたので、クラスに @Lazy(false) アノテーションを追加してみます。

@Lazy(false)
@Configuration
public class WebSecurityConfig {

その結果、テストの実行は成功し、

f:id:ksby:20200113111229p:plain

build タスクも成功するようになりました。

f:id:ksby:20200113111931p:plain

build タスクの実行時間も Spring Boot 2.1.x の Web アプリを 2.2.x へバージョンアップする ( その4 )( Spring Boot を 2.1.11 → 2.2.2 へバージョンアップする ) の時と比較すると 4分 → 3分 20秒 で 40秒程短くなりました。テストだと結構効果がありますね。

テストはこのまま -XX:TieredStopAtLevel=1-Dspring.main.lazy-initialization=true を追加しておくことにします。

履歴

2020/01/13
初版発行。